UICollectionViewCell 重新绘制具有延迟的自定义 UIView 子图层



我想在活动应用程序中显示类似于历史记录的内容,但为了解决这个问题,它是一个简单的饼图,而不是 3 个环。 我创建了一个自定义UIView并使用draw(在ctx:中)来绘制馅饼。

麻烦的是,当我滚动并重复使用单元格时,饼图会在这些单元格中保留一小会儿,然后再重新绘制。

以下是重现此内容的方法:

  1. 创建新的单一视图项目
  2. 将下面的代码复制粘贴到 ViewController.swift 和 Main.storyboard 中
  3. 构建和运行
  4. 向下滚动:你会看到一堆彩色的点。再滚动一些,您应该会看到闪烁的点。

你可能会问:

  • 这是一个简化的"日历",包含 10 个月和 30 天(单元格),只有第 2 个月有点来展示问题。
  • 我将 pieLayer 添加为 UIView 层的子层,而不是直接使用该层,因为在我的项目中,我有多个自定义层

视图控制器.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()
}
}

主故事板

<?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 and collectionView(_ collectionView:cellForItemAt:)中,它可以工作

我正在使用UIBezierPath而不是CGContext,不太确定这是否会改变任何东西。我需要确保此解决方案可以扩展到 projet 的非简化版本。

Apple Docs: API 参考

setNeedsDisplay()

应使用此方法请求仅重绘视图 当视图的内容或外观发生更改时。如果你只是 更改视图的几何图形,通常不会重绘视图。 相反,其现有内容会根据 视图的内容模式属性。重新显示现有内容 通过避免重绘具有以下特征的内容来提高性能 没有改变。

基本上 setNeedsDisplay() 在下一个绘制周期中从头开始重绘所有内容。因此,理想的方法是仅创建一次 UI 元素的实例,并在需要时更新框架或路径。它不会完全重绘所有内容,因此效率很高。

最新更新