ios - UICollectionViewCell 延迟重绘自定义 UIView 子层

标签 ios uicollectionviewcell calayer

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

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

这里是重现这个的方法:

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

你可能会问的事情:

  • 这是一个简化的“日历”,包含 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()
    }
}

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

关于ios - UICollectionViewCell 延迟重绘自定义 UIView 子层,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44156516/

相关文章:

ios - 如何手动执行通过 rx 观察到的 UITextField 编辑更改的操作?

ios - 与 completionBlock 异步下载和保存文件时 CollectionView 单元格图像错误

swift - 如何更改 UICollectionViewCell 的框架以显示点击时隐藏的按钮?

ios - 如何快速调整单元格大小以适应尺寸类别

objective-c - CALayer:线性渐变问题

ios - 线程 1 : Breakpoint 4. 1 - swift

iOS:单击单元格时展开单元格

objective-c - 将委托(delegate)分配给 CALayer 的子层会抛出 EXC_BAD_ACCESS?

ios - SceneKit CALayer 锐化绘图

ios - 如果在 iOS 9 中加载大量图像,应用程序会卡住