【问题标题】:Does animating the fill color of a `UIBezierPath` require a new scan-convert?为“UIBezierPath”的填充颜色设置动画是否需要新的扫描转换?
【发布时间】:2021-02-26 07:53:08
【问题描述】:

考虑一个国际象棋游戏。一个独立的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
            }
        }
    }
}

困难出现在"can't animate the fill color of a UIBezierPath directly"。那是因为,iOS 似乎根本不会重新渲染(也就是重新扫描转换)路径。当请求动画时,会改为执行(相对便宜的)合成操作。

【问题讨论】:

    标签: ios swift animation uiview


    【解决方案1】:

    首先,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 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()
            }
        }
    }
    

    结果:

    【讨论】:

    • 一切都好,但是代码末尾的self.backgroundColor = UIColor.yellow 会立即执行(我们知道这是预期的),但它也会立即执行。 backgroundColor 在动画期间不会从旧值淡出到新值。
    • @Sam - 也许我没有完全理解你的目标?有多种方法可以动画位置和颜色变化(以及其他属性)......我提出了另一种方法,使用中心“圆圈”的子视图。请参阅我的回答中的 Edit 2(包括对其进行操作的捕获)。
    【解决方案2】:

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

    你有两个选择:

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

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

    通常我们会使用CAShapeLayer 方法,但我提供了这两个选项供您参考。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多