UICollectionViewCell 延迟重绘自定义 UIView 子层
UICollectionViewCell redrawing custom UIView sublayers with delay
我想在 Activity 应用程序中显示类似于历史记录的内容,但为了这个问题,它是一个简单的饼图,而不是 3 个圆环。
我创建了一个自定义 UIView 并使用 draw(in ctx:) 绘制饼图。
问题在于,当我滚动并重新使用单元格时,饼图会在这些单元格中停留一小段时间,然后才会重新绘制。
重现此内容的方法如下:
- 创建一个新的单视图项目
- 复制粘贴下面的代码到ViewController.swift和Main.storyboard
- 构建 & 运行
- 向下滚动:您会看到一堆彩色圆点。再滚动一些,您应该会看到闪烁的点。
您可能会问的问题:
- 这是一个简化的 "calendar" 10 个月,30 天(单元格),只有第 2 个月有点来展示这个问题。
- 我将 pieLayer 添加为 UIView 层的子层,而不是直接使用该层,因为在我的项目中,我有不止一个自定义层
ViewController.swift
class ViewController: UICollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 10
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 30
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DayCell", for: indexPath) as! RingCell
let ring = cell.ring!
ring.pieLayer.radius = 15
ring.pieLayer.maxValue = 30
if indexPath.section == 2 {
ring.pieLayer.value = CGFloat(indexPath.row)
ring.pieLayer.segmentColor = (indexPath.row % 2 == 0 ? UIColor.green.cgColor : UIColor.red.cgColor)
} else {
ring.pieLayer.value = 0
ring.pieLayer.segmentColor = UIColor.clear.cgColor
}
ring.pieLayer.setNeedsDisplay()
return cell
}
}
class RingCell: UICollectionViewCell {
@IBOutlet weak var ring: PieView!
override func prepareForReuse() {
super.prepareForReuse()
ring.pieLayer.value = 0
ring.pieLayer.segmentColor = UIColor.clear.cgColor
ring.pieLayer.setNeedsDisplay()
}
}
open class PieView: UIView {
// MARK: Initializers
public override init(frame: CGRect) {
super.init(frame: frame)
initLayers()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initLayers()
}
// MARK: Internal initializers
var pieLayer: ProgressPieLayer!
internal func initLayers() {
pieLayer = ProgressPieLayer(centeredIn: layer.bounds)
rasterizeToScale(pieLayer)
layer.addSublayer(pieLayer)
pieLayer.setNeedsDisplay()
}
private func rasterizeToScale(_ layer: CALayer) {
layer.contentsScale = UIScreen.main.scale
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale * 2
}
}
private extension CGFloat {
var toRads: CGFloat { return self * CGFloat.pi / 180 }
}
internal class ProgressPieLayer: CAShapeLayer {
@NSManaged var value: CGFloat
@NSManaged var maxValue: CGFloat
@NSManaged var radius: CGFloat
@NSManaged var segmentColor: CGColor
convenience init(centeredIn bounds: CGRect,
radius: CGFloat = 15,
color: CGColor = UIColor.clear.cgColor,
value: CGFloat = 100,
maxValue: CGFloat = 100) {
self.init()
self.bounds = bounds
self.position = CGPoint(x: bounds.midX, y: bounds.midY)
self.value = value
self.maxValue = maxValue
self.radius = radius
self.segmentColor = color
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
let shiftedStartAngle: CGFloat = -90 // start on top
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = 360 / maxValue * value + shiftedStartAngle
ctx.move(to: center)
ctx.addArc(center: center,
radius: radius,
startAngle: shiftedStartAngle.toRads,
endAngle: angle.toRads,
clockwise: false)
ctx.setFillColor(segmentColor)
ctx.fillPath()
}
}
Main.Storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="NK3-ad-iUE">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="NFp-0o-M02">
<objects>
<collectionViewController id="NK3-ad-iUE" customClass="ViewController" customModule="UICN" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="Sy5-uf-jPK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" id="fkD-3N-K4T">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="DayCell" id="CXc-tU-7nQ" customClass="RingCell" customModule="UICN" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ic6-ea-Qzy" userLabel="Pie" customClass="PieView" customModule="UICN" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Ic6-ea-Qzy" secondAttribute="trailing" constant="-8" id="9fj-SE-D1e"/>
<constraint firstItem="Ic6-ea-Qzy" firstAttribute="top" secondItem="CXc-tU-7nQ" secondAttribute="topMargin" constant="-8" id="Hnv-yr-EBN"/>
<constraint firstItem="Ic6-ea-Qzy" firstAttribute="leading" secondItem="CXc-tU-7nQ" secondAttribute="leadingMargin" constant="-8" id="I4E-ZD-JZf"/>
<constraint firstAttribute="bottomMargin" secondItem="Ic6-ea-Qzy" secondAttribute="bottom" constant="-8" id="XOW-ao-t0L"/>
</constraints>
<connections>
<outlet property="ring" destination="Ic6-ea-Qzy" id="ZoZ-ok-TLK"/>
</connections>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="NK3-ad-iUE" id="nAW-La-2EK"/>
<outlet property="delegate" destination="NK3-ad-iUE" id="YCh-0p-7gX"/>
</connections>
</collectionView>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="6r8-g7-Adg" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-100" y="214.54272863568218"/>
</scene>
</scenes>
</document>
编辑
我可能找到了解决方案,我在 ProgressPieLayer 中创建了一个 drawPie 方法。
internal class ProgressPieLayer: CAShapeLayer {
@NSManaged var value: CGFloat
@NSManaged var maxValue: CGFloat
@NSManaged var radius: CGFloat
@NSManaged var segmentColor: CGColor
convenience init(centeredIn bounds: CGRect,
radius: CGFloat = 15,
color: CGColor = UIColor.clear.cgColor,
value: CGFloat = 100,
maxValue: CGFloat = 100) {
self.init()
self.bounds = bounds
self.position = CGPoint(x: bounds.midX, y: bounds.midY)
self.value = value
self.maxValue = maxValue
self.radius = radius
self.segmentColor = color
}
func drawPie() {
let shiftedStartAngle: CGFloat = -90 // start on top
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = 360 / maxValue * value + shiftedStartAngle
let piePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: shiftedStartAngle.toRads, endAngle: angle.toRads, clockwise: false)
piePath.addLine(to: center)
self.path = piePath.cgPath
self.fillColor = segmentColor
}
}
我打电话给
ring.pieLayer.drawPie()
在 UICollectionViewCell#prepareForReuse 和 collectionView(_ collectionView:cellForItemAt:) 中有效
我正在使用 UIBezierPath 而不是 CGContext,不太确定这是否会改变任何内容。我需要确保这个解决方案可以扩展到非简化版本的项目。
Apple Docs: API Reference
setNeedsDisplay()
You should use this method to request that a view to be redrawn only
when the content or appearance of the view change. If you simply
change the geometry of the view, the view is typically not redrawn.
Instead, its existing content is adjusted based on the value in the
view’s contentMode property. Redisplaying the existing content
improves performance by avoiding the need to redraw content that has
not changed.
基本上 setNeedsDisplay() 会在下一个绘图周期从头开始重绘所有内容。因此,理想的做法是只创建一次 UI 元素的实例,并在需要时更新框架或路径。它不会完全重绘所有内容,因此效率很高。
我想在 Activity 应用程序中显示类似于历史记录的内容,但为了这个问题,它是一个简单的饼图,而不是 3 个圆环。 我创建了一个自定义 UIView 并使用 draw(in ctx:) 绘制饼图。
问题在于,当我滚动并重新使用单元格时,饼图会在这些单元格中停留一小段时间,然后才会重新绘制。
重现此内容的方法如下:
- 创建一个新的单视图项目
- 复制粘贴下面的代码到ViewController.swift和Main.storyboard
- 构建 & 运行
- 向下滚动:您会看到一堆彩色圆点。再滚动一些,您应该会看到闪烁的点。
您可能会问的问题:
- 这是一个简化的 "calendar" 10 个月,30 天(单元格),只有第 2 个月有点来展示这个问题。
- 我将 pieLayer 添加为 UIView 层的子层,而不是直接使用该层,因为在我的项目中,我有不止一个自定义层
ViewController.swift
class ViewController: UICollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 10
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 30
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DayCell", for: indexPath) as! RingCell
let ring = cell.ring!
ring.pieLayer.radius = 15
ring.pieLayer.maxValue = 30
if indexPath.section == 2 {
ring.pieLayer.value = CGFloat(indexPath.row)
ring.pieLayer.segmentColor = (indexPath.row % 2 == 0 ? UIColor.green.cgColor : UIColor.red.cgColor)
} else {
ring.pieLayer.value = 0
ring.pieLayer.segmentColor = UIColor.clear.cgColor
}
ring.pieLayer.setNeedsDisplay()
return cell
}
}
class RingCell: UICollectionViewCell {
@IBOutlet weak var ring: PieView!
override func prepareForReuse() {
super.prepareForReuse()
ring.pieLayer.value = 0
ring.pieLayer.segmentColor = UIColor.clear.cgColor
ring.pieLayer.setNeedsDisplay()
}
}
open class PieView: UIView {
// MARK: Initializers
public override init(frame: CGRect) {
super.init(frame: frame)
initLayers()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initLayers()
}
// MARK: Internal initializers
var pieLayer: ProgressPieLayer!
internal func initLayers() {
pieLayer = ProgressPieLayer(centeredIn: layer.bounds)
rasterizeToScale(pieLayer)
layer.addSublayer(pieLayer)
pieLayer.setNeedsDisplay()
}
private func rasterizeToScale(_ layer: CALayer) {
layer.contentsScale = UIScreen.main.scale
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale * 2
}
}
private extension CGFloat {
var toRads: CGFloat { return self * CGFloat.pi / 180 }
}
internal class ProgressPieLayer: CAShapeLayer {
@NSManaged var value: CGFloat
@NSManaged var maxValue: CGFloat
@NSManaged var radius: CGFloat
@NSManaged var segmentColor: CGColor
convenience init(centeredIn bounds: CGRect,
radius: CGFloat = 15,
color: CGColor = UIColor.clear.cgColor,
value: CGFloat = 100,
maxValue: CGFloat = 100) {
self.init()
self.bounds = bounds
self.position = CGPoint(x: bounds.midX, y: bounds.midY)
self.value = value
self.maxValue = maxValue
self.radius = radius
self.segmentColor = color
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
let shiftedStartAngle: CGFloat = -90 // start on top
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = 360 / maxValue * value + shiftedStartAngle
ctx.move(to: center)
ctx.addArc(center: center,
radius: radius,
startAngle: shiftedStartAngle.toRads,
endAngle: angle.toRads,
clockwise: false)
ctx.setFillColor(segmentColor)
ctx.fillPath()
}
}
Main.Storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="NK3-ad-iUE">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="NFp-0o-M02">
<objects>
<collectionViewController id="NK3-ad-iUE" customClass="ViewController" customModule="UICN" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="Sy5-uf-jPK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" id="fkD-3N-K4T">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="DayCell" id="CXc-tU-7nQ" customClass="RingCell" customModule="UICN" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ic6-ea-Qzy" userLabel="Pie" customClass="PieView" customModule="UICN" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Ic6-ea-Qzy" secondAttribute="trailing" constant="-8" id="9fj-SE-D1e"/>
<constraint firstItem="Ic6-ea-Qzy" firstAttribute="top" secondItem="CXc-tU-7nQ" secondAttribute="topMargin" constant="-8" id="Hnv-yr-EBN"/>
<constraint firstItem="Ic6-ea-Qzy" firstAttribute="leading" secondItem="CXc-tU-7nQ" secondAttribute="leadingMargin" constant="-8" id="I4E-ZD-JZf"/>
<constraint firstAttribute="bottomMargin" secondItem="Ic6-ea-Qzy" secondAttribute="bottom" constant="-8" id="XOW-ao-t0L"/>
</constraints>
<connections>
<outlet property="ring" destination="Ic6-ea-Qzy" id="ZoZ-ok-TLK"/>
</connections>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="NK3-ad-iUE" id="nAW-La-2EK"/>
<outlet property="delegate" destination="NK3-ad-iUE" id="YCh-0p-7gX"/>
</connections>
</collectionView>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="6r8-g7-Adg" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-100" y="214.54272863568218"/>
</scene>
</scenes>
</document>
编辑
我可能找到了解决方案,我在 ProgressPieLayer 中创建了一个 drawPie 方法。
internal class ProgressPieLayer: CAShapeLayer {
@NSManaged var value: CGFloat
@NSManaged var maxValue: CGFloat
@NSManaged var radius: CGFloat
@NSManaged var segmentColor: CGColor
convenience init(centeredIn bounds: CGRect,
radius: CGFloat = 15,
color: CGColor = UIColor.clear.cgColor,
value: CGFloat = 100,
maxValue: CGFloat = 100) {
self.init()
self.bounds = bounds
self.position = CGPoint(x: bounds.midX, y: bounds.midY)
self.value = value
self.maxValue = maxValue
self.radius = radius
self.segmentColor = color
}
func drawPie() {
let shiftedStartAngle: CGFloat = -90 // start on top
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = 360 / maxValue * value + shiftedStartAngle
let piePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: shiftedStartAngle.toRads, endAngle: angle.toRads, clockwise: false)
piePath.addLine(to: center)
self.path = piePath.cgPath
self.fillColor = segmentColor
}
}
我打电话给
ring.pieLayer.drawPie()
在 UICollectionViewCell#prepareForReuse 和 collectionView(_ collectionView:cellForItemAt:) 中有效
我正在使用 UIBezierPath 而不是 CGContext,不太确定这是否会改变任何内容。我需要确保这个解决方案可以扩展到非简化版本的项目。
Apple Docs: API Reference
setNeedsDisplay()
You should use this method to request that a view to be redrawn only when the content or appearance of the view change. If you simply change the geometry of the view, the view is typically not redrawn. Instead, its existing content is adjusted based on the value in the view’s contentMode property. Redisplaying the existing content improves performance by avoiding the need to redraw content that has not changed.
基本上 setNeedsDisplay() 会在下一个绘图周期从头开始重绘所有内容。因此,理想的做法是只创建一次 UI 元素的实例,并在需要时更新框架或路径。它不会完全重绘所有内容,因此效率很高。