UICollectionViewCell 延迟重绘自定义 UIView 子层

UICollectionViewCell redrawing custom UIView sublayers with delay

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

问题在于,当我滚动并重新使用单元格时,饼图会在这些单元格中停留一小段时间,然后才会重新绘制。

重现此内容的方法如下:

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

您可能会问的问题:

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 元素的实例,并在需要时更新框架或路径。它不会完全重绘所有内容,因此效率很高。