在上一篇认识CoreAnimation中笔者介绍了系统的动画库CoreAnimation
,使用动画库有很多好处,这里就不再进行重复叙述。那么本篇将承接上一篇的内容,使用提到的基础的动画相关类来实现动画效果
效果图放上:
大体上可以看到demo主要是渐变以及形变两种动画,在更早之前的文章,我们就使用UIView
的动画接口完成过相同的动画,而这次将换成CoreAnimation
来完成这些工作
关于图层
在iOS中,每一个UIView
都拥有一个与之绑定的CALayer
图层对象,其负责视图内容的绘制与显示。跟前者一样,CALayer
也拥有树状的子图层结构,以及相似的接口方法。CALayer
是图层的基类,主要提供了视图显示范围、图层结构接口等属性,我们通过使用它的子类。下面是一段在控制器的界面中心添加一个圆形的紫色图层:
override func viewDidLoad() {
super.viewDidLoad()
let layer = CAShapeLayer()
layer.fillColor = UIColor.purpleColor().CGColor
layer.path = UIBezierPath(arcCenter: CGPoint(x: UIScreen.mainScreen().bounds.width / 2, y: UIScreen.mainScreen().bounds.height / 2), radius: 100, startAngle: 0, endAngle: 2.0*CGFloat(M_PI), clockwise: false).CGPath
self.view.layer.addSublayer(layer)
}
同样的,每一个CALayer
存在一个sublayers
的数组属性,我们也可以遍历这个数组来完成移除子视图之类的操作:
for sublayer in self.view.layer.sublayers! {
print("\(sublayer)")
sublayer.removeFromSuperlayer()
}
由于核心动画框架的动画都是基于CALayer
的图层进行添加实现的,所以图层的添加移除方法是最常用的方法。当然,还有一个addAnimation(anim:forKey:)
接口用来给图层添加动画
## 基础动画
基础动画CABasicAnimation
是最常用来实现动画效果的动画类,其继承自CAAnimation
动画基类,为图层动画效果实现了一个keyPath
属性,我们通过设置这个属性来为对应的keyPath
属性值执行动画效果。动画类提供了fromValue
和toValue
两个属性用来设置动画的起始和结束的值,比如下面一段代码让添加到视图上的紫色图层变得透明:
@IBAction func actionToAnimatedLayer(sender: AnyObject) {
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = NSNumber(double: 1)
animation.toValue = NSNumber(double: 0)
animation.duration = 1
layer.addAnimation(animation, forKey: nil)
}
上面的代码用动画表现了在1秒内让图层的opacity
属性从1
到0
的过程。但上面不难看出在动画结束之后,紫色的图层没有保持opacity
等于0
的状态,而是回到了动画最开始的状态。这是为什么呢?
在上一篇中笔者提到过在每一个CALayer
中存在着模型
、呈现
、渲染
三种图层树,正是这些图层树共同作用来完成隐式动画。那么使用核心动画的时候,实际上CABasicAnimation
会根据动画时长计算出每一帧的动画属性的值,然后实时提交给呈现树
来展示对应时间点的视图效果,在动画结束时CAAnimation
对象会自动从图层上移除。而由于在整个动画过程模型树
的值没有改变,所以在动画结束的时候呈现树
会再次从模型树
获取图层的属性重新绘制。对此,存在这几种解决方案:
-
在实现动画的时候同时修改
opacity
,保证模型树的数据同步@IBAction func actionToAnimatedLayer(sender: AnyObject) { let animation = CABasicAnimation(keyPath: "opacity") animation.fromValue = NSNumber(double: 1) animation.toValue = NSNumber(double: 0) animation.fillMode = kCAFillModeForwards animation.removedOnCompletion = false animation.duration = 1 layer.addAnimation(animation, forKey: nil) }
-
取消
CAAnimation
的自动移除,并且设置在动画结束后保持动画的结束状态@IBAction func actionToAnimatedLayer(sender: AnyObject) { let animation = CABasicAnimation(keyPath: "opacity") animation.fromValue = NSNumber(double: 1) animation.toValue = NSNumber(double: 0) animation.fillMode = kCAFillModeForwards animation.removedOnCompletion = false animation.duration = 1 layer.addAnimation(animation, forKey: nil) }
-
实现动画代理方法。综合上面两种方法的操作
@IBAction func actionToAnimatedLayer(sender: AnyObject) { let animation = CABasicAnimation(keyPath: "opacity") animation.fromValue = NSNumber(double: 1) animation.toValue = NSNumber(double: 0) animation.fillMode = kCAFillModeForwards animation.removedOnCompletion = false animation.duration = 1 animation.setValue(layer, forKey: "animatedLayer") animation.delegate = self layer.addAnimation(animation, forKey: nil) } override func animationDidStop(anim: CAAnimation, finished flag: Bool) { if anim is CABasicAnimation { let animation = anim as! CABasicAnimation if let layer = animation.valueForKey("animatedLayer") as? CALayer { layer.setValue(animation.toValue, forKey: animation.keyPath!) layer.removeAllAnimations() } } }
相比较前两种方法,实现代理然后设置属性的做法有些繁杂且无用的感觉。但在某些应用场景下,我们需要在动画结束时移除图层或其他操作,通过实现代理是最好的做法。其他常用的keyPath
动画值可以在这里查看
动画组
接着上面的动画效果,我想要在渐变的基础上增加一个形变动画,那么我需要创建两个CABasicAnimation
对象来完成这一工作:
@IBAction func actionToAnimatedLayer(sender: AnyObject) {
let opacity = CABasicAnimation(keyPath: "opacity")
opacity.fromValue = NSNumber(double: 1)
opacity.toValue = NSNumber(double: 0)
opacity.duration = 1
layer.addAnimation(opacity, forKey: "opacity")
let scale = CABasicAnimation(keyPath: "transform")
scale.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
scale.toValue = NSValue(CATransform3D: CATransform3DMakeScale(2, 2, 2))
scale.duration = 1
layer.addAnimation(scale, forKey: "scale")
}
除了上面这段代码之外,在CoreAnimation
框架中提供了一个CAAnimationGroup
类来将多个动画对象整合成一个对象添加到图层上。从使用实现的角度而言,并不会跟上面的代码有任何出入,却可以让代码的逻辑更加清晰:
@IBAction func actionToAnimatedLayer(sender: AnyObject) {
// create animations
let group = CAAnimationGroup()
group.animations = [opacity, scale]
group.duration = 1
layer.addAnimation(group, forKey: "group")
}
按钮动画
首先是动画中的形变和透明渐变分别对应transform
以及opacity
两个keyPath
,其次,动画图层不是按钮本身的图层,因此还需要添加额外的一个图层。另外,动画存在外扩和内扩的动画效果,因此我们还需要定义一个枚举来区分:
enum LXDAnimationType {
case Inner
case Outer
}
在swift的extension
中不支持添加储值属性,因此我们需要使用到runtime
的动态绑定来完成对按钮包括动画类型、动画颜色两个属性的扩充:
private var kAnimationTypeKey: UInt = 0
private var kAnimationColorKey: UInt = 1
extension UIButton {
enum LXDAnimationType {
case Inner
case Outer
}
//MARK: - Expand property
var animationType: LXDAnimationType? {
get {
if let type = (objc_getAssociatedObject(self, &kAnimationTypeKey) as? String) {
return LXDAnimationType(rawValue: type)
}
return nil
}
set {
guard newValue != nil else { return }
self.clipsToBounds = (newValue == .Inner)
objc_setAssociatedObject(self, &kAnimationTypeKey, newValue!.rawValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var animationColor: UIColor {
get {
if let color = objc_getAssociatedObject(self, &kAnimationColorKey) {
return color as! UIColor
}
return UIColor.whiteColor()
}
set {
objc_setAssociatedObject(self, &kAnimationColorKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
接下来是如何保证我们在点击按钮的时候可以执行我们的动画。这里我们通过重写按钮的sendAction(action:to:forEvent:)
方法来执行动画,这个方法在每次按钮发送一个事件时会被调用。同理,当用户点击按钮时也会调用这个方法:
//MARK: - Override
public override func sendAction(action: Selector, to target: AnyObject?, forEvent event: UIEvent?) {
super.sendAction(action, to: target, forEvent: event)
if let type = animationType {
var rect: CGRect?
var radius = self.layer.cornerRadius
var pos = touchPoint(event)
let smallerSize = min(self.frame.width, self.frame.height)
let longgerSize = max(self.frame.width, self.frame.height)
var scale = longgerSize / smallerSize + 0.5
switch type {
case .Inner:
radius = smallerSize / 2
rect = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
break
case .Outer:
scale = 2.5
pos = CGPoint(x: self.bounds.width/2, y: self.bounds.height/2)
rect = CGRect(x: pos.x - self.bounds.width, y: pos.y - self.bounds.height, width: self.bounds.width, height: self.bounds.height)
break
}
let layer = animateLayer(rect!, radius: radius, position: pos)
let group = animateGroup(scale)
self.layer.addSublayer(layer)
group.setValue(layer, forKey: "animatedLayer")
layer.addAnimation(group, forKey: "buttonAnimation")
}
}
public override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let layer = anim.valueForKey("animatedLayer") as? CALayer {
layer .removeFromSuperlayer()
}
}
//MARK: - Private
private func touchPoint(event: UIEvent?) -> CGPoint {
if let touch = event?.allTouches()?.first {
return touch.locationInView(self)
} else {
return CGPoint(x: self.frame.width/2, y: self.frame.height/2)
}
}
private func animateLayer(rect: CGRect, radius: CGFloat, position: CGPoint) -> CALayer {
let layer = CAShapeLayer()
layer.lineWidth = 1
layer.position = position
layer.path = UIBezierPath(roundedRect: rect, cornerRadius: radius).CGPath
switch animationType! {
case .Inner:
layer.fillColor = animationColor.CGColor
layer.bounds = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
break
case .Outer:
layer.strokeColor = animationColor.CGColor
layer.fillColor = UIColor.clearColor().CGColor
break
}
return layer
}
private func animateGroup(scale: CGFloat) -> CAAnimationGroup {
let opacityAnim = CABasicAnimation(keyPath: "opacity")
opacityAnim.fromValue = NSNumber(double: 1)
opacityAnim.toValue = NSNumber(double: 0)
let scaleAnim = CABasicAnimation(keyPath: "transform")
scaleAnim.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
scaleAnim.toValue = NSValue(CATransform3D: CATransform3DMakeScale(scale, scale, scale))
let group = CAAnimationGroup()
group.animations = [opacityAnim, scaleAnim]
group.duration = 0.5
group.delegate = self
group.fillMode = kCAFillModeBoth
group.removedOnCompletion = false
return group
}
扩展之后的按钮只要设置animationType
这个属性之后就会实现在点击时的动画效果
animateButton.animationType = .Outer
尾话
相比起国外的应用,国内的动画效果要显得内敛得多,甚至很多的app是没考虑过动画制作的。但是在移动端开发已然是一片血海的今天,漂亮的动画效果仍然会为你的应用带来留存,前提是你的应用要靠谱——单纯的动效留不住人。因此,掌握动画是至关重要的一项基本技能。本文demo