引言
本篇博客紧接着上一篇的隐式动画开始介绍显式动画。隐式动画是创建动态页面的一种简单的直接的方式,也是UIKit的动画机制基础。但是它并不能涵盖所有的动画类型。
显式动画
接下来我们就来研究另外一种动画显式动画,它能够对一些属性做指定的动画,或者非线性动画。
显式动画 - 属性动画
给一个图层的属性添加动画时,我们需要借助CAAnimationDelegate代理,这个代理我们可以在CAAnimation的文件中找到,它有两个方法:
动画已经开始
optional func animationDidStart(_ anim: CAAnimation)
动画已经结束
optional func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
下面的例子中我们使用属性动画来修改图层的颜色,并在动画结束之后来设置图层最终颜色。
这里面有一个需要注意的地方,如果我们给单独的图层添加动画,需要在一个新的事物中来进行,并且禁用图层的默认行为,否则动画会发生两次,一次是我们设置的显式动画,一个是图层默认的隐式动画。
代码如下:
class ViewController: UIViewController, CAAnimationDelegate{
let colorLayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
colorLayer.frame = CGRect(x: 50, y: 100, width: 100, height: 100)
colorLayer.backgroundColor = UIColor.red.cgColor
self.view.layer.addSublayer(colorLayer)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let colors = [UIColor.red.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor]
let animation = CABasicAnimation()
animation.keyPath = "backgroundColor"
animation.toValue = colors.randomElement()
animation.duration = 1.0
animation.delegate = self
colorLayer.add(animation, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let animation = anim as? CABasicAnimation {
CATransaction.begin()
CATransaction.setDisableActions(true)
let cgColor = animation.toValue
colorLayer.backgroundColor = (cgColor as! CGColor)
CATransaction.commit()
}
}
}
动画的结束事件,是使用代理的方法来实现的,这样会有一个问题。就是假如一个控制器中有多个图层进行动画,那么所有的动画都会使用这一个回调方法,这样的话我们就需要判断是哪个图层动画的回调。
在添加动画时我们发现open func add(_ anim: CAAnimation, forKey key: String?)有一个key属性,目前设置的为nil。事实上它就是东湖的唯一标识符,我们给它设置一个非空的唯一字符串,在回调的时候就可以对所有图层进行循环,调用-animationForKey:来比对结果。
我们来修改一下代码:
colorLayer.add(animation, forKey: "colorChange")
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if colorLayer.animation(forKey: "colorChange") == anim {
if let animation = anim as? CABasicAnimation {
CATransaction.begin()
CATransaction.setDisableActions(true)
let cgColor = animation.toValue
colorLayer.backgroundColor = (cgColor as! CGColor)
CATransaction.commit()
}
}
}
但是当有很多很多图层有很多很多动画的话,这个方法就显得不太优雅了。
不过我们还有更简单的版本。CAAnimation也显示了KVC协议,所以我们可以使用open func setValue(_ value: Any?, forKey key: String)和open func value(forKey key: String) -> Any?两个方法来存取属性。但是CAAnimation还有一个不同的地方,它更新时一个NSDictionary,我们可以随意设置键值对。
这就意味着我们可以对动画打任何类型的标签,下面我们就来写个例子感受一下这个特性:
class ViewController: UIViewController, CAAnimationDelegate{
let firstView = UIView()
let secondView = UIView()
let thirdView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(firstView)
firstView.backgroundColor = .red
firstView.frame = CGRect(x: 40.0, y: self.view.bounds.size.height - 300.0, width: 40.0, height: 10.0)
self.view.addSubview(secondView)
secondView.backgroundColor = .green
secondView.frame = CGRect(x: 80.0, y: self.view.bounds.size.height - 300.0, width: 40.0, height: 10.0)
self.view.addSubview(thirdView)
thirdView.backgroundColor = .blue
thirdView.frame = CGRect(x: 120.0, y: self.view.bounds.size.height - 300.0, width: 40.0, height: 10.0)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
addAnimation(view: firstView, height: 100.0)
addAnimation(view: secondView, height: 200.0)
addAnimation(view: thirdView, height: 300.0)
}
func addAnimation(view:UIView,height:CGFloat) {
let animation = CABasicAnimation()
animation.keyPath = "bounds.size.height"
animation.toValue = height
animation.duration = 1.0
animation.delegate = self
animation.setValue(view, forKey: "view")
view.layer.add(animation, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard let anim = anim as? CABasicAnimation else {
return
}
if let view = anim.value(forKey: "view") as? UIView {
let height = anim.toValue as? CGFloat
view.layer.bounds.size.height = height!
}
}
}
点击屏幕后发现,每个动画的结果都对应到了自己的图层,效果如下:
可以做动画的属性和虚拟属性非常的多,我们就来列举一些常见的几何属性吧
- tranform.rotation.x 按x轴旋转的弧度
- tranform.rotation.y 按y轴旋转的弧度
- tranform.rotation.z 按z轴旋转的弧度
- tranform.rotation 按z轴旋转的弧度,和tranform.rotation.z效果一样
- tranform.scale.x 在x轴按比例放大缩小
- tranform.scale.y 在y轴按比例放大缩小
- tranform.scale.z 在z轴按比例放大缩小
- tranform.scale 整体按比例放大缩小
- transform.translation.x 沿x轴平移
- transform.translation.y 沿y轴平移
- transform.translation.z 沿z轴平移
- transform.translation x,y坐标均发生改变
- transform CATransform3D 4xbounds4矩阵
- bounds layer大小
- position layer位置
- anchorPoint 锚点位置
- cornerRadius 圆角大小
- ZPosition Z轴位置
显式动画 - 关键帧动画
CAKeyframeAnimation是另一种UIKit没有暴露出来的且功能十分强大的类。和CABasicAnimation一样它也是继承自CAPropertyAnimation,并且它也作用于单一的一个属性,不同的是它不是只设置一个起始值和一个结束值。而是可以设置一连串随意的值来做动画。
关键帧动画起源于传动动画。它的主要思想是指主导的动画在显著改变发生时重绘当前帧,也就是关键帧。而关键帧与关键帧之间的绘制则由Core Animation通过推算来完成。
我们来使用一个例子,通过关键帧动画来修改图层的颜色,代码如下:
class ViewController: UIViewController, CAAnimationDelegate{
let colorLayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
colorLayer.frame = CGRect(x: 50, y: 100, width: 100, height: 100)
colorLayer.backgroundColor = UIColor.red.cgColor
self.view.layer.addSublayer(colorLayer)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let animation = CAKeyframeAnimation()
animation.keyPath = "backgroundColor"
animation.duration = 2.0
animation.values = [UIColor.red.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor,UIColor.yellow.cgColor]
animation.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
colorLayer.add(animation, forKey: nil)
}
效果如下:
CAKeyframeAnimation还有另外一种方式来指定动画,就是使用CGPath。path属性可以用一种更直观的方式来描述动画,我们这次采用UIKit提供的UIBezierPath来创建path,并且为了更直观我们使用CAShapeLayer将path渲染出来。然后我们在设置CAKeyframeAnimation来创建我们的动画。
代码如下:
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 50, y: 150))
bezierPath.addCurve(to: CGPoint(x: 300, y: 150), controlPoint1: CGPoint(x: 150, y: 50), controlPoint2: CGPoint(x: 200, y: 250))
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 3.0
self.view.layer.addSublayer(shapeLayer)
let layer = CALayer()
layer.frame = CGRect(x: 50 - 40, y: 150 - 40, width: 40, height: 40)
layer.contents = UIImage(named: "a-meiguihuahuameigui")?.cgImage
self.view.layer.addSublayer(layer)
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.path = bezierPath.cgPath
animation.duration = 4.0
layer.add(animation, forKey: nil)
效果如下:
还可以体验一下animation.rotationMode = .rotateAuto这个属性。
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.path = bezierPath.cgPath
animation.duration = 4.0
animation.rotationMode = .rotateAuto
layer.add(animation, forKey: nil)
你会发现,花会沿着曲线运动时会发生旋转,使自己总垂直于曲线。
结语
本篇博客介绍了两个最常用的显式动画,属性动画和关键帧动画。大家在实际开发过程中应该经常会使用到。接下来的博客我们会继续讨论显式动画,进行动画的组合,过渡动画,以及取消动画。