iOS核心动画高级技巧(笔记)——(六)隐式动画

动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先来讨论框架自动完成的隐式动画(除非你明确禁用了这个功能)。

事务

  • 事务是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
    事务是通过CATransaction类来做管理,可以用+begin+commit分别来入栈或者出栈。CATransaction+begin+commit方法与UIView+beginAnimations:context:+commitAnimations方法类似。
    任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。

完成块

  • 基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。来看一个例子,首先创建一个蓝色的方块,然后添加一个按钮,随机改变它的颜色,再添加一个完成之后的block,用来在每次颜色变化结束之后切换到另一个旋转45度的动画。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变。并且旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。
    var colorLayer = CALayer()
    @IBOutlet weak var layerView: UIView!
    @IBAction func changeColor(sender: UIButton) {
    CATransaction.begin()
    CATransaction.setAnimationDuration(2.0)

      let red = CGFloat(arc4random()) / CGFloat(INT_MAX)
      let green = CGFloat(arc4random()) / CGFloat(INT_MAX)
      let blue = CGFloat(arc4random()) / CGFloat(INT_MAX)
      self.colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).CGColor
      
      CATransaction.setCompletionBlock({
          var transform = self.colorLayer.affineTransform()
          transform = CGAffineTransformRotate(transform, CGFloat(M_PI_2) / 2.0)
          self.colorLayer.setAffineTransform(transform)
          
      })
      
      CATransaction.commit()
    }
    override func viewDidLoad() {
      self.colorLayer.frame = CGRectMake(50, 50, 100, 100)
      self.colorLayer.backgroundColor = UIColor.blueColor().CGColor
      self.layerView.layer.addSublayer(self.colorLayer)
    }
    
gif

图层行为

  • 对上一个例子做一点修改,移除colorLayer,并且直接设置layerView关联图层的背景色,现在是直接对UIView关联的图层做动画而不是一个单独的图层。运行程序,会发现当按下按钮,图层颜色瞬间切换到新的值,而不是之前平滑过渡的动画。这是因为隐式动画被UIVew关联图层给禁用了。
    UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第七章)。对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。
    CATransacition有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。
    我们来对颜色渐变的例子使用一个不同的行为,通过给colorLayer设置一个自定义的actions字典。 行为通常是一个被Core Animation隐式调用的显式动画对象。这里我们使用的是一个实现了CATransaction的实例,叫做推进过渡。第七章中将会详细解释过渡,不过对于现在,知道CATransition响应CAAction协议,并且可以当做一个图层行为就足够了。结果是不论在什么时候改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。
    @IBOutlet weak var layerView: UIView!
    var colorLayer = CALayer()

    @IBAction func changeColor(sender: UIButton) {
      CATransaction.begin()
      CATransaction.setAnimationDuration(2)
      
      let red = CGFloat(arc4random() / UINT32_MAX)
      let green = CGFloat(arc4random()) / CGFloat(INT_MAX)
      let blue = CGFloat(arc4random()) / CGFloat(INT_MAX)
      self.colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).CGColor
      
      CATransaction.commit()
    }
    override func viewDidLoad() {
      self.colorLayer.frame = CGRectMake(50, 50, 100, 100)
      self.colorLayer.backgroundColor = UIColor.blueColor().CGColor
      self.layerView.layer.addSublayer(self.colorLayer)
      
      let transition = CATransition()
      transition.type = kCATransitionPush
      transition.subtype = kCATransitionFromLeft
      self.colorLayer.actions = ["backgroundColor" : transition] 
    }
    
推进过渡

呈现与模型

  • 在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。
    每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
    一个移动的图层是如何通过数据模型呈现的

    大多数情况下,你不需要直接访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。
    1.如果你在实现一个基于定时器的动画(见第10章“基于定时器的动画”),而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
    2.如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    let point = (touches as NSSet).anyObject()?.locationInView(self.view)
    if ((self.colorLayer.presentationLayer()!.hitTest(point!)) != nil) {
    }
    }

推荐阅读更多精彩内容