在其绘制方法中为 CALayer 内容设置动画

Animate a CALayer contents in its draw method

好的,这个问题可能已经在 SO 上被问过 1000 次了。我一定已经尝试了 500 个答案。没有任何效果,这让我觉得这可能是不可能的。

在我开始之前,我不想要动画自定义 属性,我只想利用边界变化。大多数答案只针对前者。但是,如果我的内容仅取决于图层的大小,仅此而已呢?

这真的不可能吗? 我试过了:

我达到的最远:内容在动画开始时绘制,然后在结束时绘制。当从大变小时,哪个当然看起来很糟糕(也像素化)。

不一定重要,但这里是绘制方法供参考:

override func draw(in ctx: CGContext) {
        UIGraphicsPushContext(ctx)
        paths = [Dimension: [UIBezierPath]]()
        cuts = [Dimension: [UIBezierPath]]()

        let centerTranslate = CGAffineTransform.init(translationX: frame.width * 0.5,
                                                     y: frame.height * 0.5)
        let dimRotateAndTranslate = centerTranslate.rotated(by: CGFloat.pi / 2)

        let needlePath = UIBezierPath(rect: CGRect(x: 0,
                                                   y: 0,
                                                   width: circularSpacing,
                                                   height: frame.height * 0.5 - centerRadius))

        let piProgress = CGFloat(progress * 2 * Float.pi)
        var needleRotateAndTranslate = CGAffineTransform(rotationAngle: piProgress)
        needleRotateAndTranslate = needleRotateAndTranslate.translatedBy(x: -needlePath.bounds.width * 0.5,
                                                                         y: -needlePath.bounds.height - centerRadius)
        needlePath.apply(needleRotateAndTranslate.concatenating(centerTranslate))

        let sortedDims = states.sorted(by: {[=10=].key < .key})
        for (offset: i, element: (key: dim, value: beats)) in sortedDims.enumerated() {
            var pathsAtDim = paths[dim] ?? []
            var cutAtDim = cuts[dim] ?? []
            let thickness = (frame.width * 0.5 - centerRadius - circularSpacing * CGFloat(states.count - 1)) / CGFloat(states.count)
            let xSpacing = thickness + circularSpacing
            let radius = centerRadius + thickness * 0.5 + CGFloat(i) * xSpacing
            for (offset: j, beat) in beats.enumerated() {
                let length = CGFloat.pi * 2 / CGFloat(beats.count) * CGFloat(beat.relativeDuration)
                var path = UIBezierPath(arcCenter: .zero,
                                        radius: radius,
                                        startAngle: CGFloat(j) * length,
                                        endAngle: CGFloat(j + 1) * length,
                                        clockwise: true)
                path = UIBezierPath(cgPath: path.cgPath.copy(strokingWithWidth: thickness,
                                                             lineCap: .butt,
                                                             lineJoin: .miter,
                                                             miterLimit: 0,
                                                             transform: dimRotateAndTranslate))
                func cutAtIndex(index: Int, offset: CGFloat) -> UIBezierPath {
                    let cut = UIBezierPath(rect: CGRect(x: 0,
                                                        y: 0,
                                                        width: thickness,
                                                        height: circularSpacing))
                    let offsetRadians = offset / radius
                    let x = (radius - thickness / 2) * cos(CGFloat(index) * length + offsetRadians)
                    let y = (radius - thickness / 2) * sin(CGFloat(index) * length + offsetRadians)
                    let translate = CGAffineTransform.init(translationX: x,
                                                           y: y)
                    let rotate = CGAffineTransform.init(rotationAngle: CGFloat(index) * length)
                    cut.apply(rotate
                        .concatenating(translate)
                        .concatenating(dimRotateAndTranslate))
                    return cut
                }

                cutAtDim.append(cutAtIndex(index: j, offset: -circularSpacing * 0.5))

                pathsAtDim.append(path)
            }
            paths[dim] = pathsAtDim
            cuts[dim] = cutAtDim
        }

        let clippingMask = UIBezierPath.init(rect: bounds)
        for (dim, _) in states {
            for cut in cuts[dim] ?? [] {
                clippingMask.append(cut.reversing())
            }
        }
        clippingMask.addClip()
        for (dim, states) in states {
            for i in 0..<states.count {
                let state = states[i]
                if let path = paths[dim]?[i] {
                    let color = dimensionsColors[dim]?[state.state] ?? .darkGray
                    color.setFill()
                    path.fill()
                }
            }
        }

        ctx.resetClip()

        needleColor.setFill()
        needlePath.fill()

        UIGraphicsPopContext()
    }

首先,这个问题在很多方面都不是很清楚。如果我的理解是正确的,您希望更改边界会在动画期间自动重绘 CALayer。

方法一: 它可以通过一般方式实现,包括设置计时器和稍微更改边界(从 CGRECT 数组)并每次重绘。由于可以访问绘图功能,所以自己开发这样的动画应该不是问题。

方法二: 这个想法和以前一样,但是如果你想利用 CAAnimation 的方式。以下代码段可以为您提供帮助。 在这一个中,使用了一个外部定时器 (CADisplayLink)。此外,为了方便起见,必须通过函数 updateBounds 实现边界更改。

请记住,应谨慎对待片段的结构。任何修改都可能导致动画丢失。

成功的结果会提示你在bounds changing的过程中继续重绘

测试viewController:

  class ViewController: UIViewController {

         override func viewDidAppear(_ animated: Bool) {
   super.viewDidAppear(animated)
    layer1 = MyLayer()
    layer1.frame = CGRect.init(x: 0, y: 0, width: 300, height: 500)
    layer1.backgroundColor = UIColor.red.cgColor
    view.layer.addSublayer(layer1)
    layer1.setNeedsDisplay()
}

  @IBAction   func updateLayer1(){
    CATransaction.begin()
    CATransaction.setAnimationDuration(3.0)

   let nextBounds = CGRect.init(origin: layer1.bounds.origin, size:    CGSize.init(width: layer1.bounds.width + 100, height: layer1.bounds.height + 150))
   layer1.updateBounds(nextBounds)
   CATransaction.commit()
   }
 }

CALayer的子类,只列出边界变化动画部分

 class MyLayer: CALayer, CAAnimationDelegate{

func updateBounds(_ bounds: CGRect){
    nextBounds = bounds
    self.bounds = bounds
}
private var nextBounds : CGRect!

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if let keyPath = (anim as! CABasicAnimation).keyPath  , let displayLink = animationLoops[keyPath]{
        displayLink?.invalidate()
      animationLoops.removeValue(forKey: keyPath)    }
}

@objc func updateDrawing(displayLink: CADisplayLink){
   bounds = presentation()!.bounds
   setNeedsDisplay()
}

 var animationLoops: [String : CADisplayLink?] = [:]

  override func action(forKey event: String) -> CAAction? {
    let result = super.action(forKey: event)
    if(event == "bounds"){
       guard let result = result  as? CABasicAnimation,
        animationLoops[event] == nil
        else{return nil}
       let anim = CABasicAnimation.init(keyPath: result.keyPath)
          anim.fromValue = result.fromValue
          anim.toValue = nextBounds
         let caDisplayLink = CADisplayLink.init(target: self, selector: #selector(updateDrawing))
        caDisplayLink.add(to: .main,   forMode: RunLoop.Mode.default)
        anim.delegate = self
        animationLoops[event] = caDisplayLink
        return anim
    }
  return result
}

希望这就是您想要的。祝你有个愉快的一天。

比较动画: 没有定时器:

添加 CADsiaplayLinker 计时器: