动画化 `UIBezierPath` 的填充颜色是否需要新的扫描转换?

Does animating the fill color of a `UIBezierPath` require a new scan-convert?

考虑一个国际象棋游戏。一个自包含的 ViewController 和一个 ChessPieceView 出现在下面。 (在我的 Xcode 10 上,Playgrounds 不支持图形,因此使用了一个成熟的项目,如果很小的话。)

我们为棋子的动作制作动画。我们还想为围绕它的圆圈的颜色设置动画(用于国际象棋教程等)。

这里我们设置动画 View.backgroundColor。但我们真正想做的是为圆圈的填充颜色设置动画。作品的 backgroundColor 将是 from/to 方块的填充颜色,无论它们是什么。

//  ViewController.swift
import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let rect = CGRect(origin: CGPoint(x: 100, y: 100),
                          size: CGSize(width: 100, height: 100))
        let chessPieceView = ChessPieceView.init(fromFrame: rect)
        self.view.addSubview(chessPieceView)
    }
}


//  ChessPieceView.swift
import UIKit
class ChessPieceView: UIView {
    init(fromFrame frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.cyan
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func draw(_ rect: CGRect) {
        let circle = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
                                  radius: 30,
                                  startAngle: 0, endAngle: CGFloat.pi*2,
                                  clockwise: false)
        UIColor.red.setFill()
        circle.fill()
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                    action: #selector(tapPiece(_ :))))
    }
    @objc func tapPiece(_ gestureRecognizer : UITapGestureRecognizer ) {
        guard gestureRecognizer.view != nil else { return }
        if gestureRecognizer.state == .ended {
            let animator = UIViewPropertyAnimator(duration: 1.2,
                                                  curve: .easeInOut,
                                                  animations: {
                                                    self.center.x += 100
                                                    self.center.y += 200
            })
            animator.startAnimation()
            UIView.animate(withDuration: 1.2) {
                self.backgroundColor = UIColor.yellow
            }
        }
    }
}

难度出现一个。那是因为,iOS 似乎根本不会重新渲染(又名重新扫描转换)路径。当请求动画时,将执行(相对便宜的)合成操作。

首先,layoutSubviews()绝对不是放置addGestureRecognizer()的地方......应该是在初始化期间完成。

其次,使用 CAShapeLayer 而不是覆盖 draw().

可能会获得更好的结果

试一试:

class ChessPieceView: UIView {
    let shapeLayer = CAShapeLayer()
    
    init(fromFrame frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.cyan
        layer.addSublayer(shapeLayer)
        shapeLayer.fillColor = UIColor.red.cgColor
        addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                    action: #selector(tapPiece(_ :))))
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        let r: CGFloat = 30
        let rect = CGRect(x: bounds.midX - r, y: bounds.midY - r, width: r * 2, height: r * 2)
        shapeLayer.path = UIBezierPath(ovalIn: rect).cgPath
    }
    @objc func tapPiece(_ gestureRecognizer : UITapGestureRecognizer ) {
        guard gestureRecognizer.view != nil else { return }
        if gestureRecognizer.state == .ended {
            let animator = UIViewPropertyAnimator(duration: 1.2,
                                                  curve: .easeInOut,
                                                  animations: {
                                                    self.center.x += 100
                                                    self.center.y += 200
                                                    self.backgroundColor = UIColor.yellow
                                                  })
            animator.startAnimation()
        }
    }
}

编辑

重新阅读你的问题,你似乎想为圆圈的填充颜色设置动画。

在这种情况下,您可以使用形状图层作为蒙版。此 class 将绘制一个红色填充的圆圈...点击它将为其位置设置动画并设置从红到黄颜色变化的动画:

class ChessPieceView: UIView {
    let shapeLayer = CAShapeLayer()
    
    init(fromFrame frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.red
        shapeLayer.fillColor = UIColor.black.cgColor
        addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                    action: #selector(tapPiece(_ :))))
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        let r: CGFloat = 30
        let rect = CGRect(x: bounds.midX - r, y: bounds.midY - r, width: r * 2, height: r * 2)
        shapeLayer.path = UIBezierPath(ovalIn: rect).cgPath
        layer.mask = shapeLayer
    }
    @objc func tapPiece(_ gestureRecognizer : UITapGestureRecognizer ) {
        guard gestureRecognizer.view != nil else { return }
        if gestureRecognizer.state == .ended {
            let animator = UIViewPropertyAnimator(duration: 1.2,
                                                  curve: .easeInOut,
                                                  animations: {
                                                    self.center.x += 100
                                                    self.center.y += 200
                                                    self.backgroundColor = UIColor.yellow
                                                  })
            animator.startAnimation()
        }
    }
}

编辑 2

此示例使用子视图提供一个青色矩形,中心有一个圆形红色视图...每次点击都会为矩形的位置及其中心的圆圈颜色设置动画:

class ChessViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let rect = CGRect(origin: CGPoint(x: 50, y: 100),
                          size: CGSize(width: 100, height: 100))
        let chessPieceView = ChessPieceView.init(fromFrame: rect)
        self.view.addSubview(chessPieceView)
    }
}


//  ChessPieceView.swift

class ChessPieceView: UIView {
    let circleView = UIView()
    let shapeLayer = CAShapeLayer()
    var startCenter: CGPoint!
    
    init(fromFrame frame: CGRect) {
        super.init(frame: frame)
        startCenter = self.center
        backgroundColor = UIColor.cyan
        addSubview(circleView)
        circleView.backgroundColor = .red
        addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                    action: #selector(tapPiece(_ :))))
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        let r: CGFloat = 30
        circleView.frame = CGRect(x: bounds.midX - r, y: bounds.midY - r, width: r * 2, height: r * 2)
        circleView.layer.cornerRadius = r
    }
    @objc func tapPiece(_ gestureRecognizer : UITapGestureRecognizer ) {
        guard gestureRecognizer.view != nil else { return }
        if gestureRecognizer.state == .ended {
            let targetCenter: CGPoint = self.center.x == startCenter.x ? CGPoint(x: startCenter.x + 100, y: startCenter.y + 200) : startCenter
            let targetColor: UIColor = self.circleView.backgroundColor == .red ? .yellow : .red
            let animator = UIViewPropertyAnimator(duration: 1.2,
                                                  curve: .easeInOut,
                                                  animations: {
                                                    self.center = targetCenter
                                                    self.circleView.backgroundColor = targetColor
                                                  })
            animator.startAnimation()
        }
    }
}

结果:

正确,手动渲染的 UIBezierPath 不能直接设置动画。

您有两个选择:

  1. 不是使用自定义 draw(_:) 的视图 strokes/fills 路径,而是添加 CAShapeLayer 并设置其 path。该形状层的 fillColor 是可动画的(通过 CABasicAnimation)。

  2. 如果您想使用手动 draw(_:) 实现动画视图(或动画任何不可动画的 属性),您可以调用 transition(with:duration:options:animations:completion:)。它基本上拍摄了“之前”视图的快照,并动画化了从该快照到新再现的过渡。

一般我们会使用CAShapeLayer的方式,但是我提供了两种方式供大家参考。