collectionView 中的 CAShapelayer 在 reloadData() 上添加奇怪的动画

CAShapelayer in collectionView adding odd animation on reloadData()

我构建了一个 collectionView,它在每个单元格中显示一个圆形图(以展示百分比)。为此,我在 CustomCell 中投射的子类 (CircularGraph) 中绘制 CAShapelayer。

我遇到的问题是,每当我重新加载我的 collectionView 时,CAShapelayers 都会重新绘制到它们的 strokeEnd 位置,并在这样做时添加不需要的动画。看起来他们是根据上一层的 strokeEnd 值绘制的。这让我相信这与我的子类无法转换图形的离散版本有关。

为了演示这个问题,我构建了一个小型演示项目,可用 here。只需点击重新加载按钮即可查看我指的是什么。每当加载视图时也会发生这种情况,例如切换选项卡时(我想这是有道理的,因为 collectionView 会再次加载它的数据)。

这是集合视图:

class CellsController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

  let graphValues:[CGFloat] = [0.12, 0.35, 0.14, 1, 0.89]

  override func viewDidLoad() {
    super.viewDidLoad()
    print("Showing cellsController")
    collectionView?.backgroundColor = .lightGray
    navigationItem.title = "Cells"
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Reload", style: .plain, target: self, action: #selector(didPressReload))
    collectionView?.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
  }

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
    cell.graph.progressLayerStrokeEnd = graphValues[indexPath.row]
    return cell
  }

  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return graphValues.count
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: view.frame.width, height: 156)
  }

  @objc func didPressReload() {
    collectionView?.reloadData()
  }
}

这是正在投射图形的 CustomCell:

class CustomCell: UICollectionViewCell {

  let graph: CircularGraph = {
    let graph = CircularGraph()
    graph.translatesAutoresizingMaskIntoConstraints = false
    return graph
  }()

  override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = .white

    addSubview(graph)
    graph.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
    graph.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    graph.heightAnchor.constraint(equalToConstant: 100).isActive = true
    graph.widthAnchor.constraint(equalToConstant: 100).isActive = true
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

这是图表的子类:

class CircularGraph: UIView {

  //All layers
  let trackLayer = CAShapeLayer()
  let progressLayer = CAShapeLayer()

  //Animation values
  var percentageValue = CGFloat()

  //Line width
  var lineWidth: CGFloat = 15 { didSet { updatePath() } }

  //Fill colors
  var trackLayerFillColor: UIColor = .clear { didSet { trackLayer.fillColor = trackLayerFillColor.cgColor } }
  var progressLayerFillColor: UIColor = .clear { didSet { progressLayer.fillColor = progressLayerFillColor.cgColor } }

  //Stroke colors
  var trackStrokeColor: UIColor = UIColor.lightGray { didSet { trackLayer.strokeColor = trackStrokeColor.cgColor  } }
  var progressLayerStrokeColor: UIColor = UIColor.green { didSet { progressLayer.strokeColor = progressLayerStrokeColor.cgColor } }

  //Stroke start and end
  var trackLayerStrokeStart: CGFloat = 0 { didSet { trackLayer.strokeStart = trackLayerStrokeStart } }
  var progressLayerStrokeStart: CGFloat = 0 { didSet { progressLayer.strokeStart = progressLayerStrokeStart } }

  var trackLayerStrokeEnd:   CGFloat = 1 { didSet { trackLayer.strokeEnd = trackLayerStrokeEnd } }
  var progressLayerStrokeEnd:   CGFloat = 1 { didSet { progressLayer.strokeEnd = progressLayerStrokeEnd } }

  override init(frame: CGRect) {
    super.init(frame: frame)
    configure()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    configure()
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    updatePath()
  }  

  func configure() {
    trackLayer.strokeColor = trackStrokeColor.cgColor
    trackLayer.fillColor = trackLayerFillColor.cgColor
    trackLayer.strokeStart = trackLayerStrokeStart
    trackLayer.strokeEnd  = trackLayerStrokeEnd

    progressLayer.strokeColor = progressLayerStrokeColor.cgColor
    progressLayer.fillColor = progressLayerFillColor.cgColor
    progressLayer.strokeStart = progressLayerStrokeStart
    progressLayer.strokeEnd  = progressLayerStrokeEnd

    layer.addSublayer(trackLayer)
    layer.addSublayer(progressLayer)
  }

  func updatePath() {
    //The actual calculation for the circular graph
    let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
    let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
    let circularPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)

    trackLayer.path = circularPath.cgPath
    trackLayer.lineWidth = lineWidth

    progressLayer.path = circularPath.cgPath
    progressLayer.lineWidth = lineWidth
    progressLayer.lineCap = kCALineCapRound

    //Set the frame in order to rotate the outer circular paths to start at 12 o'clock
    trackLayer.transform = CATransform3DIdentity
    trackLayer.frame = bounds
    trackLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)

    progressLayer.transform = CATransform3DIdentity
    progressLayer.frame = bounds
    progressLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
  }
}

我找到了一个基于 CATransaction 的修复程序,您可以遵循这个想法:

var progressLayerStrokeEnd: CGFloat = 1 {
    didSet {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        progressLayer.strokeEnd = progressLayerStrokeEnd
        CATransaction.commit()
    }
}

基本上,每次修改CAShapeLayer,都应该禁用隐式CAShapeLayer 动画。因此,您可以根据需要调整 setDisableActions 的标志,例如:在调用 reloadData

时将其设置为 true