iOS Developer - AnimationiOS AnimationiOS动画

iOS动画-认识CoreAnimation

2016-06-07  本文已影响2634人  sindri的小巢

前言

在iOS中,普通的动画可以使用UIKit提供的方法来实现动画,但如果想要实现复杂的动画效果,使用CoreAnimation框架提供的动画效果是最好的选择。那么两种动画方案相比之下,后者存在的主要好处包括不仅下面这些:

核心动画
CoreAnimation框架下,最主要的两个部分是图层CALayer以及动画CAAnimation类。前者管理着一个可以被用来实现动画的位图上下文;后者是一个抽象的动画基类,它提供了对CAMediaTimingCAAction协议的支持,方便子类实例直接作用于CALayer本身来实现动画效果。接下来笔者会分段分别讲述上面提到的类,参考信息来自于苹果官方文档以及objc中国

CALayer

CALayer类结构

如果你喜欢动画效果,在网上开源的动画实现中总是能看到CALayer及其子类的应用,那么了解这个图层类别先从它的结构看起(此处列出了了部分属性并且去除了注释):

public class CALayer : NSObject, NSCoding, CAMediaTiming {
    
    public func presentationLayer() -> AnyObject?
    public func modelLayer() -> AnyObject

    public var bounds: CGRect
    public var position: CGPoint
    public var anchorPoint: CGPoint
    public var transform: CATransform3D
    public var frame: CGRect
    public var hidden: Bool

    public var superlayer: CALayer? { get }
    public func removeFromSuperlayer()
    public func addSublayer(layer: CALayer)
    public func insertSublayer(layer: CALayer, below sibling: CALayer?)
    public func insertSublayer(layer: CALayer, above sibling: CALayer?)
    public func replaceSublayer(layer: CALayer, with layer2: CALayer)
    public var sublayerTransform: CATransform3D

    public var mask: CALayer?
    public var masksToBounds: Bool

    public func hitTest(p: CGPoint) -> CALayer?
    public func containsPoint(p: CGPoint) -> Bool

    public var shadowColor: CGColor?
    public var shadowOpacity: Float
    public var shadowOffset: CGSize
    public var shadowRadius: CGFloat

    public var contents: AnyObject?
    public var contentsRect: CGRect

    public var cornerRadius: CGFloat
    public var borderWidth: CGFloat
    public var borderColor: CGColor?
    public var opacity: Float
}

根据CALayer Class Reference中的描述,在每一个UIView的背后都有一个CALayer对象用来协助它显示内容,它自身管理着我们提供给视图显示的位图上下文以及保存这些位图上下文的几何信息。通过上面的代码可以看出:

对于苹果为什么要把UIViewCALayer区分开来,网上已经有了一篇很详(qi)细(pa)的文章讲解这个问题都有了CALayer,为什么还要UIView

图层树和隐式动画

在每一个CALayer中,都有三个重要的层次树,它们负责相互协调完成图层的渲染展示效果。这三个层次树分别是:

CALayer中的显示数据几乎都是可动画属性,这个特性为我们制作核心动画提供了很大的实践基础。在一个单独的CALayer中(也就是说这个layer并没有和任何UIView绑定),我们修改它的显示属性的时候,都会触发一个从旧值新值之间的简单动画效果,这种动画我们称之为隐式动画:

class ViewController: UIViewController {

    let layer = CAShapeLayer()

    override func viewDidLoad() {
        super.viewDidLoad()
        layer.strokeEnd = 0
        layer.lineWidth = 6
        layer.fillColor = UIColor.clearColor().CGColor
        layer.strokeColor = UIColor.redColor().CGColor
        self.view.layer.addSublayer(layer)
    }

    @IBAction func actionToAnimate() {
        layer.path = UIBezierPath(arcCenter: self.view.center, radius: 100, startAngle: 0, endAngle: 2*CGFloat(M_PI), clockwise: true).CGPath
        layer.strokeEnd = 1
    }
}

可以看到上面的代码中我单独创建了一个CALayer并且将它添加到当前的控制器视图的图层上,strokeEnd这一属性表示填充百分比。当这个属性发生变化的时候,产生了一个画圈的动画效果:

隐式动画
在隐式动画的实现背后,隐藏着一个最重要的扮演角色CAAction协议这一过程会在下面进行详细的介绍。那么在上面这个隐式动画的过程中,模型树和呈现树发生了哪些变化呢?由于系统的动画时长默认为0.25秒,我设置一个0.05秒的定时器在每次回调的时候查看一下这两个层次树的信息:
@IBAction func actionToAnimate() {
    timer = NSTimer(timeInterval: 0.05, target: self, selector: #selector(timerCallback), userInfo: nil, repeats: true)
    NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSRunLoopCommonModes)
    layer.path = UIBezierPath(arcCenter: self.view.center, radius: 100, startAngle: 0, endAngle: 2*CGFloat(M_PI), clockwise: true).CGPath
    layer.strokeEnd = 1
}

@objc private func timerCallback() {
    print("========================\nmodelLayer: \t\(layer.modelLayer().strokeEnd)\ntpresentationLayer: \t\(layer.presentationLayer()!.strokeEnd)")
    if fabs((layer.presentationLayer()?.strokeEnd)! - 1) < 0.01 {
        if let _ = timer {
            timer?.invalidate()
            timer = nil
        }
    }
}

控制台的输出结果如下:

========================
modelLayer:     1.0
presentationLayer:  0.294064253568649
========================
modelLayer:     1.0
presentationLayer:  0.676515340805054
========================
modelLayer:     1.0
presentationLayer:  0.883405208587646
========================
modelLayer:     1.0
presentationLayer:  0.974191427230835
========================
modelLayer:     1.0
presentationLayer:  0.999998211860657

可以看到当一个隐式动画发生的时候,modelLayer的属性被修改成动画最终的结果值。而系统会根据动画时长和最终效果值计算出动画中每一帧的数值,然后依次更新设置到presentationLayer当中。最终这些计算工作都完成之后,渲染树renderingTree根据这些值将动画效果渲染到屏幕上。

那么通过层次树我们能制作什么呢?假设我需要制作下面这么一个粘性的弹球动画,那么我在界面的最左侧、最右侧以及中间各自添加了一个CALayer,当点击按钮的时候给左右两侧的layer添加一个匀速的position下移动画,中间的centerLayer添加一个弹簧动画。通过使用定时器更新获取这三个layer的呈现树y轴坐标来绘制区域形成这样一个动画:

粘性弹球菜单动画

其他属性

除了上面重点介绍的属性之外,下面的属性我只进行简单的介绍,详细的使用以及动画作用会在以后对应使用的动画中更详细的讲解:

CAAnimation

CAAnimation的子类

开头说过,CAAnimation是一个封装出来的基类,其最重要的目的在于遵循两个重要的动画相关协议,所以解析动画类型要从它的子类依赖关系下手。在苹果文档中,CAAnimation的直接子类包括这些:

CAAnimation的子类
从图中我们可以看到存在这么三个子类:

类结构属性

从上面的图中我们可以看到CAAnimation遵循了两个协议,在其本身属性中并没有太多的属性。其中大部分的动画相关属性都是在协议中声明的,在实现中动态生成了settergetter

public class CAAnimation : NSObject, NSCoding, NSCopying, CAMediaTiming, CAAction {

    public class func defaultValueForKey(key: String) -> AnyObject?
    public func shouldArchiveValueForKey(key: String) -> Bool

    public var timingFunction: CAMediaTimingFunction?

    public var delegate: AnyObject?

    public var removedOnCompletion: Bool
}

通过CAAnimation的类结构,可以分为属性和方法两个部分,其中属性是我们需要重点关注的

动画协议属性

除了CAAnimation本身的属性之外,另外两个协议中声明了决定动画时长、前后动画效果等关键属性:

public protocol CAMediaTiming {

    public var beginTime: CFTimeInterval { get set }

    public var duration: CFTimeInterval { get set }

    public var speed: Float { get set }

    public var timeOffset: CFTimeInterval { get set }
    
    public var repeatCount: Float { get set }

    public var repeatDuration: CFTimeInterval { get set }

    public var autoreverses: Bool { get set }

    public var fillMode: String { get set }
}

CAMediaTiming是一个控制动画时间的协议,提供了动画过程中的时间相关的属性,对于这些属性在控制动画时间一文中讲解的非常清楚了,笔者在这里就不再一一介绍。此外,还有另一个协议CAAction协议:

public protocol CAAction {

    /* Called to trigger the event named 'path' on the receiver. The object
     * (e.g. the layer) on which the event happened is 'anObject'. The
     * arguments dictionary may be nil, if non-nil it carries parameters
     * associated with the event. */

    @available(iOS 2.0, *)
    public func runActionForKey(event: String, object anObject: AnyObject, arguments dict: [NSObject : AnyObject]?)
}

在动画发生之后这个方法会被调用,这个方法把将要发生的事件告诉图层,从而让图层做出对应的操作,比如渲染等。

CAAction

显式动画

开头笔者提到过隐式动画是单独的layer的可动画属性发生改变时自动产生的过度动画,那么肯定就有对应的显式动画。显式动画的制作过程是创建一个动画对象,然后添加到实现动画的图层上。下面这段代码创建了一个修改layer.position.y的基础动画对象,然后设置动画结束值为160后添加到layer层上,动画的默认时长是0.25秒:

let animation = CABasicAnimation(keyPath: "position.y")
animation.toValue = NSNumber(float: 160)
layer.position.y = 160
layer.addAnimation(animation, forKey: nil)

对于动画的更详细讲解不在本篇文章的计划内,在接下来的核心动画中笔者会更加详细的介绍各式各样的CAAnimation子类以用于不同的动画场景。这里放上上面粘性弹窗的核心代码:

func startAnimation() {
    let displayLink = CADisplayLink(target: self, selector: #selector(fluctAnimation(_:)))
    displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
    let move = CABasicAnimation(keyPath: "position.y")
    move.toValue = NSNumber(float: 160)
    leftLayer.position.y = 160
    rightLayer.position.y = 160
    leftLayer.addAnimation(move, forKey: nil)
    rightLayer.addAnimation(move, forKey: nil)
    
    let spring = CASpringAnimation(keyPath: "position.y")
    spring.damping = 15
    spring.initialVelocity = 40
    spring.toValue = NSNumber(float: 160)
    centerLayer.position.y = 160
    centerLayer.addAnimation(spring, forKey: "spring")
}

给三个图层添加了下移的动画之后,创建CADisplayLink定时器来同步屏幕刷新频率更新弹出效果:

@objc private func fluctAnimation(link: CADisplayLink) {
    let path = UIBezierPath()
    path.moveToPoint(CGPointZero)
    
    guard let _ = centerLayer.animationForKey("spring") else {
        return
    }
    
    let offset = leftLayer.presentationLayer()!.position.y - centerLayer.presentationLayer()!.position.y
    var controlY: CGFloat = 160
    if offset < 0 {
        controlY = centerLayer.presentationLayer()!.position.y + 30
    } else if offset > 0 {
        controlY = centerLayer.presentationLayer()!.position.y - 30
    }
    
    path.addLineToPoint(leftLayer.presentationLayer()!.position)
    path.addQuadCurveToPoint(rightLayer.presentationLayer()!.position, controlPoint: CGPoint(x: centerLayer.position.x, y: controlY))
    path.addLineToPoint(CGPoint(x: UIScreen.mainScreen().bounds.width, y: 0))
    path.closePath()
    fluctLayer.path = path.CGPath
}

隐式动画发生了什么

上面说过隐式动画发生在单独的CALayer对象的可动画属性被改变时。如果我们这个layer已经存在与之绑定的UIView对象,那么当我们直接修改这个layer的属性的时候,只会瞬间从旧值变成新值的显示效果,不会有额外的效果。在CoreAnimation的编程指南中对此做出了解释:UIView默认情况下禁止了layer动画,但在animate block中重新启用了它们。这是我们看到的行为,但如果认真去挖掘这一机制的内部实现,我们会惊讶于viewlayer之间协同工作的精心设计,这里就要提到CAAction


当任何一个可动画的layer的属性发生改变的时候,layer通过向它的代理人发送actionForLayer(layer:event:)方法来查询一个对应属性变化的CAAction对象,这个方法可以返回下面三个结果:

正常来说,当一个CALayerUIView关联的时候,这个UIView对象会成为layer的代理人。因此从返回值上来说,当layer的属性被我们修改的时候,这个关联的UIView对象一般都是直接返回NSNull对象,而只有在animate block的状态下才会返回实际的动画效果,方便让图层继续查找处理动作的方案。我们通过代码来验证:

print("===========normal call===========")
print("\(self.view.actionForLayer(self.view.layer, forKey: "opacity"))")
UIView.animateWithDuration(0.25) {
    print("===========animate block call===========")
    print("\(self.view.actionForLayer(self.view.layer, forKey: "opacity"))")
}

控制台输出结果如下,在动画block中确实返回了一个CABasicAnimation对象来协同完成这个动画效果

===========normal call===========
Optional(<null>)
===========animate block call===========
Optional(<CABasicAnimation:0x7f8712d30c90; delegate = <UIViewAnimationState: 0x7f8712d304d0>; fillMode = both; timingFunction = easeInEaseOut; duration = 0.25; fromValue = 1; keyPath = opacity>)

通常来说处在动画代码块中的UIView都会返回这么一个核心动画对象,但如果返回的是nil,图层还有继续查找其他的动作解决方案,整个的查找过程共有四次,这个在CALayer的头文件中已经说明了:


layer对象查找到了属性修改动作的动画时,就会调用addAnimation(_:forKey:)方法开始执行动画效果。同样的,我们继承CALayer对象来重写这个方法:
class LXDActionLayer: CALayer {
    override func addAnimation(anim: CAAnimation, forKey key: String?) {
        print("***********************************************")
        print("Layer will add an animation: \(anim)")
        super.addAnimation(anim, forKey: key)
    }
}

class LXDActionView: UIView {

    override class func layerClass() -> AnyClass {
        return LXDActionLayer.classForCoder()
    }
}

override func viewDidLoad() {
    super.viewDidLoad()

    let actionView = LXDActionView()
    view.addSubview(actionView)
    print("===========normal call===========")
    print("\(self.view.actionForLayer(self.view.layer, forKey: "opacity"))")
    actionView.layer.opacity = 0.5
    UIView.animateWithDuration(0.25) {
        print("===========animate block call===========")
        print("\(self.view.actionForLayer(self.view.layer, forKey: "opacity"))")
    actionView.layer.opacity = 0
}

控制台输出结果如下:

===========normal call===========
Optional(<null>)
===========animate block call===========
Optional(<CABasicAnimation:0x7f8f00eb1850; delegate = <UIViewAnimationState: 0x7f8f00eb0e90>; fillMode = both; timingFunction = easeInEaseOut; duration = 0.25; fromValue = 1; keyPath = opacity>)
***********************************************
Layer will add an animation: <CABasicAnimation: 0x7f8f00eb2400>

这里可能会有人有疑惑,为什么两次输出的CABasicAnimation的地址不一样。为了保证同一个动画对象可以作用于多个CALayer对象执行,在addAnimation(_:forKey:)方法调用的时候都会对CAAnimation对象进行一次copy操作。各位可以继承CABasicAnimation对象重写copy方法自行测试

尾言

本来笔者想要直接使用动画粒子开始讲解核心动画这一框架,但是考虑到如果能够全面的对核心动画进行一次讲解,这对于以后的文章讲解以及动画粒子的制作有很大的帮助。本文demo

上一篇:碎片动画
下一篇:按钮动画

转载请注明本文作者和转载地址

上一篇下一篇

猜你喜欢

热点阅读