[关闭]
@zhangyuhangk 2015-08-12T15:01:44.000000Z 字数 14185 阅读 3545

Core Animation Advanced Techniques 学习笔记 (二)

CoreAnimation


教程:https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques

本来想写成一篇笔记就好,但是发现写太长了,机子会变得很卡……不知道是不是Cmd Markdown的bug。
分成两篇应该差不多了

7. 隐式动画

默认情况下,Core Animation会开启隐式动画,动画时间为0.25秒。CALayer的一些特定属性(文档内会标记为Animatable),被修改了值之后,就会触发隐式动画。

CATransaction

CATransaction是动画的事务。
设置属性前一定要调用CATransaction.begin(),设置后一定要调用CATransaction.commit()。
begincommit相当于事务的入栈和出栈。
系统在每一帧开始时都会调用begin,最后调用commit,这就实现了隐式动画。
可以显式地调用begin再压入一个栈,这样可以修改动画的一些参数(比如时间),而不影响其它动画。比如:

  1. CATransaction.begin()
  2. CATransaction.setAnimationDuration(3)
  3. colorLayer.backgroundColor = UIColor.redColor.CGColor
  4. CATransaction.commit()

setCompletionBlock方法: 可以设置一个block作为动画全部结束时的通知。
注意: 必须在设置属性之前调用。如果在最后commit前才调用setCompletionBlock,则block会被马上执行!

The completion block object that is guaranteed to be called (on the main thread) as soon as all animations subsequently added by this transaction group have completed (or have been removed.)

根据文档的说明,如果在setCompletionBlock之后没有动画添加,则直接执行block。

CAAction

setDisableActions: 禁用隐式动画

CALayer的属性被修改时,会向delegate寻求对应的action(实现了CAAction接口的类型,如CABasicAnimation),寻找的路径依次为:

  • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的actionForLayer:forKey方法。如果有,直接调用并返回结果。
  • 如果没有委托,或者委托没有实现actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
  • 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
  • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的defaultActionForKey:方法。

UIView实现了actionForLayer:forKey方法,并返回了nil,所以一般情况下,UIView的动画是禁用的。除非调用了animateWithDuration之类的动画函数。这些函数会在block之前执行beginAnimations:context:函数,最后再执行commitAnimations。在这两个调用之间,actionForLayer:forKey会返回非空值以产生动画。

对于单独的layer,一般可以用两种方式来修改隐式动画效果

  1. let transition = CATransition()
  2. transition.type = kCATransitionPush
  3. transition.subtype = kCATransitionFromLeft
  4. colorLayer.actions = ["backgroundColor": transition]

以上代码设置了backgroundColor的过渡效果,变成从左向右滑入的动画

presentation layer

presentationLayer: 呈现图层。
设置属性时,layer上的属性会马上设置成新值,但界面上却没有马上显示当前值,而是在两个值之间逐帧过渡过来。这个中间状态就保存在呈现图层。presentationLayer属性可以返回layer对应的呈现图层,而呈现图层的modelLayer属性则返回layer。这是一个微型的MVC,layer就是model,presentation layer就是view。
presentation layer
下面的例子显示一个蓝色小方块。点击空白处,方块移动到该位置。在移动的过程中,如果点中方块,方块随机换一种颜色。

  1. lazy var colorLayer: CALayer = {
  2. let layer = CALayer()
  3. layer.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
  4. layer.position = self.view.center
  5. layer.backgroundColor = UIColor.blueColor().CGColor
  6. return layer
  7. }()
  8. func testColorAnimation() {
  9. view.layer.addSublayer(colorLayer)
  10. }
  11. override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
  12. let point = touches.first!.locationInView(view)
  13. let presentationLayer = colorLayer.presentationLayer() as! CALayer
  14. if presentationLayer.hitTest(point) != nil {
  15. colorLayer.backgroundColor = randomColor().CGColor
  16. } else {
  17. CATransaction.begin()
  18. CATransaction.setAnimationDuration(4)
  19. colorLayer.position = point
  20. CATransaction.commit()
  21. }
  22. }
  23. func randomColor() -> UIColor {
  24. return UIColor(red: randomPercent(), green: randomPercent(), blue: randomPercent(), alpha: 1)
  25. }
  26. func randomPercent() -> CGFloat {
  27. return CGFloat(random() % 256) / 255
  28. }

8. 显式动画

UIView中默认禁用了隐匿动画,所以需要动画的地方需要显示地指定。

CABasicAnimation

基本动画。
CABasicAnimation的继承路径:
CABasicAnimation -> CAPropertyAnimation -> CAAnimation
removedOnCompletion: CAAnimation的属性,默认为true。表示动画结束时自己移除动画对象。

主要属性:

这几个都是AnyObject?类型,其可能的类型有:

使用方法如下:

  1. let animation = CABasicAnimation()
  2. animation.duration = 1
  3. animation.keyPath = "transform"
  4. animation.toValue = NSValue(CATransform3D: CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 0, 1))
  5. view.layer.addAnimation(animation, forKey: nil)

以上代码可以实现旋转view的动画,但是动画执行完,view又回到原来的状态。这是因为动画类只影响了呈现层,而没有把最终值应用 到layer本身。
由于CAAnimation支持KVC,可以加以下代码,在动画完成时赋值。
需要实现CAAnimationDelegate协议,这又是个不存在声明的协议,只能在文档中找到说明。

  1. animation.delegate = self
  2. animation.setValue(view, forKey: "animatedView")

然后在动画结束时赋值

  1. override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
  2. let view = anim.valueForKey("animatedView") as! UIView
  3. let basicAnimation = anim as! CABasicAnimation
  4. let value = basicAnimation.toValue as! NSValue
  5. view.layer.transform = value.CATransform3DValue
  6. }

CAKeyframeAnimation

关键帧动画。可以指定多个变化的值。
主要属性:

虚拟属性

keyPath可以设置为一些『虚拟』的属性,比如:transform.rotation
具体可用的值如下:

虚拟属性的计算是通过CAPropertyAnimation的valueFunction属性(类型是CAValueFunction)来换算成CATransform3D的

动画组

CAAnimationGroup的主要属性是animations,可以添加多个动画。每个子动画可以指定各自的时间,group本身也可以设定duration。当group的duration时间到或者所有子动画结束时,整个group结束。

过渡

对于没有标记为Animatable的属性的变化,需要用到过渡来做动画。过渡相当于整个layer画面的切换(layer实例没变)
CATransition主要属性:

使用下面的代码来添加过渡效果:

  1. let transition = CATransition()
  2. transition.type = kCATransitionPush
  3. transition.subtype = kCATransitionFromBottom
  4. imageView.layer.addAnimation(transition, forKey: nil)

addAnimation:forKey:函数中,只要传入的是CATransition类型,后面的key会被置为kCATransition,无论传入什么值。所以对同一个layer,一次只能指定一个过渡效果。

UIView提供了两个函数,可以方便地进行过渡动画:

这两个函数的options参数是UIViewAnimationsOptions类型,选项比较丰富(比CATransition的type提供的类型多,不知道为啥):

(layer中的renderInContext:函数可以把当前画面绘制到context上,可以再调用UIGraphicsGetImageFromCurrentImageContext()获得UIImage)

取消动画

可以使用removeAnimationForKey:或removeAllAnimations取消动画。
被取消的动画仍然会触发animationDidStop:finished:函数,后面的finished标志会传入false

9. 图层时间

时间控制是通过CAMediaTiming协议实现。CALayer和CAAnimation都实现了该协议。

主要属性

层级时间

动画的时间是按动画的层级关系呈树状依赖(就像坐标系统一样)。

对CALayer或者CAGroupAnimation调整duration和repeatCount/repeatDuration属性并不会影响到子动画。但是beginTime,timeOffset和speed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayer和CAGroupAnimation的speed属性将会对动画以及子动画速度应用一个缩放的因子。

已经添加的动画无法再修改属性(会抛异常),所以可以通过修改其所属的layer来间接控制动画。
以下代码将layer的speed设置为0,使动画暂停。然后通过修改timeOffset到手动控制动画的进度。

  1. lazy var curvePath: UIBezierPath = {
  2. let path = UIBezierPath()
  3. path.moveToPoint(self.pointFromCenter(0, -300))
  4. path.addCurveToPoint(self.pointFromCenter(0, 300),
  5. controlPoint1: self.pointFromCenter(-300, 0),
  6. controlPoint2: self.pointFromCenter(300, 0))
  7. return path
  8. }()
  9. lazy var colorLayer: CALayer = {
  10. let layer = CALayer()
  11. layer.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
  12. layer.position = self.view.center
  13. layer.backgroundColor = UIColor.blueColor().CGColor
  14. return layer
  15. }()
  16. func testAnimationControl() {
  17. view.layer.addSublayer(colorLayer)
  18. let shape = CAShapeLayer()
  19. shape.path = curvePath.CGPath
  20. shape.fillColor = UIColor.clearColor().CGColor
  21. shape.strokeColor = UIColor.redColor().CGColor
  22. shape.lineWidth = 3
  23. shape.lineCap = kCALineCapRound
  24. view.layer.addSublayer(shape)
  25. let animation = CAKeyframeAnimation(keyPath: "position")
  26. animation.duration = 1
  27. animation.path = curvePath.CGPath
  28. colorLayer.addAnimation(animation, forKey: nil)
  29. colorLayer.speed = 0
  30. view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "controlAnimation:"))
  31. }
  32. func controlAnimation(gesture: UIPanGestureRecognizer) {
  33. let sy = gesture.translationInView(view).y
  34. let percent = sy / 600
  35. colorLayer.timeOffset = max(0, min(0.999, colorLayer.timeOffset + CFTimeInterval(percent)))
  36. gesture.setTranslation(CGPoint.zeroPoint, inView: view)
  37. }

在屏幕上拖动就可以控制动画了。
(问题:这样会影响这个layer上的所有动画,如果我只想控制一个动画呢?)

10. Easing

CAMediaTimingFunction

对于隐式动画讲,动画就是一个属性在两个值之间的变化过程。比如说移动(position)。它可以是匀速运动,也可以是加速或减速运动。这就涉及计算每一帧位置的算法。对于CAAnimation,就是timingFunction。这是个CAMediaTimingFunction类型,它有一个init(name: String)构造函数,可以传入以下值:

此处输入图片的描述
(x轴是时间,y轴是速度)

单独的CALayer隐式动画使用的默认值是kCAMediaTimingFunctionDefault,而UIView里的layer的默认值是kCAMediaTimingFunctionEaseInEaseOut.

UIView的过渡函数

前面说过的UIView过渡函数的options参数也有相对应的几个值:

CAKeyframeAnimation

CAKeyframeAnimation有个timingFunctions属性,可以传多个CAMediaTimingFunction。其数量必须为values的数量减1,因为它是用来计算每两个值之间的过渡动画的。也可以直接指定timingFunction(没有s),则所有动画都用这个算法。

自定义CAMediaTimingFunction

CAMediaTimingFunction的『速度/时间』曲线可以由『3次贝塞尔曲线』描述(如上面的图)
构造函数如下:

  1. init(controlPoints c1x: Float,
  2. _ c1y: Float,
  3. _ c2x: Float,
  4. _ c2y: Float)

『3次贝塞尔曲线』涉及4个点,起点和终点为(0, 0)和(1, 1),这里只需传入中间的两个控制点。

可以使用CAKeyframeAnimation把多个曲线动画连在一起,如下:

  1. func testMultipleKeyframesAnimation() {
  2. view.layer.addSublayer(colorLayer)
  3. colorLayer.position.y -= 300
  4. let p = colorLayer.position
  5. let animation = CAKeyframeAnimation(keyPath: "position")
  6. animation.duration = 6
  7. animation.values = [
  8. NSValue(CGPoint: p),
  9. NSValue(CGPoint: CGPoint(x: p.x + 100, y: p.y + 200)),
  10. NSValue(CGPoint: CGPoint(x: p.x + 100, y: p.y + 400)),
  11. NSValue(CGPoint: CGPoint(x: p.x, y: p.y + 600)),
  12. ]
  13. animation.timingFunctions = [
  14. CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut),
  15. CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear),
  16. CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn),
  17. ]
  18. animation.keyTimes = [0, 0.2, 0.8, 1]
  19. colorLayer.addAnimation(animation, forKey: nil)
  20. }

以上动画共有4个关键点(包含起点和终点),所以需要3个timing function,另外还可指定每帧的时间点。
keyTimes属性一般来说是以0开始,以1结束,中间每个数字要大于或等于前一个数字。每个数字代表一个时间比例。数组的数量要与values一致。具体限制还受calculationMode影响,可以看文档。

上面的实现方法比较死板且不易维护,对于复杂的动画(比如球的多次弹跳)可以通过公式事先生成一系列的关键帧来实现。
这个网站有提供一些算法
http://robertpenner.com/easing/

借助上述网站的bounceEaseOut算法,实现小球弹跳3次至静止的动画:

  1. lazy var ballLayer: CALayer = {
  2. let ball = CALayer()
  3. ball.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
  4. ball.position = self.view.center
  5. ball.cornerRadius = 50
  6. ball.backgroundColor = UIColor.greenColor().CGColor
  7. return ball
  8. }()
  9. func testBouncingBall() {
  10. view.layer.addSublayer(ballLayer)
  11. view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "dropBallToHere:"))
  12. }
  13. func dropBallToHere(gesture: UITapGestureRecognizer) {
  14. let beginPosition = ballLayer.position
  15. let destinationPosition = gesture.locationInView(view)
  16. let duration: CFTimeInterval = 2
  17. var positions = [CGPoint]()
  18. let frames = Int(duration * 60)
  19. for i in 0..<frames {
  20. var time = CGFloat(i) / CGFloat(frames)
  21. time = bounceEaseOut(time)
  22. positions.append(interpolateFromPoint(beginPosition, toPoint: destinationPosition, time: time))
  23. }
  24. let animation = CAKeyframeAnimation(keyPath: "position")
  25. animation.duration = duration
  26. animation.values = positions.map{ NSValue(CGPoint: $0) }
  27. ballLayer.addAnimation(animation, forKey: nil)
  28. CATransaction.begin()
  29. CATransaction.setDisableActions(true)
  30. ballLayer.position = destinationPosition
  31. CATransaction.commit()
  32. }
  33. func interpolateFromPoint(fromPoint: CGPoint, toPoint: CGPoint, time: CGFloat) -> CGPoint {
  34. let x = interpolateFromValue(fromPoint.x, toValue: toPoint.x, time: time)
  35. let y = interpolateFromValue(fromPoint.y, toValue: toPoint.y, time: time)
  36. return CGPoint(x: x, y: y)
  37. }
  38. func interpolateFromValue(fromValue: CGFloat, toValue: CGFloat, time: CGFloat) -> CGFloat {
  39. return fromValue + (toValue - fromValue) * time
  40. }

注意:iOS的帧率是60/s,这里使用duration * 60来计算关键帧数。这属于比较土的办法。

NSTimer

可以通过NSTimer要手动绘制动画(不通过CAAnimation)。把时间设置为1/60秒,然后每帧调用timing function计算位置。但是NSTimer并不能保证帧率。更好的方案是用CADisplayLink。

CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。

CACurrentMediaTime函数返回当前“马赫时间”,即设备启动后的时间。
把上面的代码改成用CADisplayLink实现

  1. var runningAnimation = false
  2. var fromPosition: CGPoint!
  3. var toPosition: CGPoint!
  4. var duration: CFTimeInterval = 0
  5. var startTime: CFTimeInterval = 0
  6. func testCADisplayLink() {
  7. view.layer.addSublayer(ballLayer)
  8. view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "dropBall:"))
  9. let timer = CADisplayLink(target: self, selector: "dropBallStep:")
  10. timer.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
  11. }
  12. func dropBall(gesture: UITapGestureRecognizer) {
  13. guard !runningAnimation else {
  14. print("animation is not completed yet.")
  15. return
  16. }
  17. fromPosition = ballLayer.position
  18. toPosition = gesture.locationInView(view)
  19. duration = 2
  20. startTime = CACurrentMediaTime()
  21. runningAnimation = true
  22. }
  23. func dropBallStep(timer: CADisplayLink) {
  24. guard runningAnimation else { return }
  25. let pastTime = CACurrentMediaTime() - startTime
  26. let time = bounceEaseOut(CGFloat(pastTime / duration))
  27. guard time <= 1 else {
  28. runningAnimation = false
  29. return
  30. }
  31. CATransaction.begin()
  32. CATransaction.setDisableActions(true)
  33. let position = interpolateFromPoint(fromPosition, toPoint: toPosition, time: time)
  34. ballLayer.position = position
  35. CATransaction.commit()
  36. }

此处把CADisplayLink加入到main run loop里了,后面的run loop mode代表优先级,有3种可能值:

优先级如果设置得太高,有可能导致其它动画不能动。

物理引擎

教材中直接使用了物理引擎chipmunk,因为作者写这本书时用的还是iOS6,现在已经可以使用iOS7引入的UIDynamic框架了。这部分内容较多,另开一篇在这里


后面的内容基本是讲优化了,放到第三篇里。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注