动画化 `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
不能直接设置动画。
您有两个选择:
不是使用自定义 draw(_:)
的视图 strokes/fills 路径,而是添加 CAShapeLayer
并设置其 path
。该形状层的 fillColor
是可动画的(通过 CABasicAnimation
)。
如果您想使用手动 draw(_:)
实现动画视图(或动画任何不可动画的 属性),您可以调用 transition(with:duration:options:animations:completion:)
。它基本上拍摄了“之前”视图的快照,并动画化了从该快照到新再现的过渡。
一般我们会使用CAShapeLayer
的方式,但是我提供了两种方式供大家参考。
考虑一个国际象棋游戏。一个自包含的 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
}
}
}
}
难度出现一个
首先,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
不能直接设置动画。
您有两个选择:
不是使用自定义
draw(_:)
的视图 strokes/fills 路径,而是添加CAShapeLayer
并设置其path
。该形状层的fillColor
是可动画的(通过CABasicAnimation
)。如果您想使用手动
draw(_:)
实现动画视图(或动画任何不可动画的 属性),您可以调用transition(with:duration:options:animations:completion:)
。它基本上拍摄了“之前”视图的快照,并动画化了从该快照到新再现的过渡。
一般我们会使用CAShapeLayer
的方式,但是我提供了两种方式供大家参考。