《iOS 三问》 -- 从动画系统的实现谈 iOS 核心动画
Core Animation 库是 iOS 动画技术的基础,由一系列类与子类组成(他们基本都有个特点就是以各种 CA 开头,如 CALayer,CAAnimation)。我们之前学习到的 View 动画,隐式 Layer 动画等,都是基于 Core Animation
库的一个封装实现。Core Animation
又称显式动画,使用显式动画技术,我们可以更细致地定义我们要的动画的整个实现过程。
通过本章,我希望和大家一起不仅掌握 iOS 的动画编程,更掌握 UI 系统关于动画编程的核心技术与探索核心技术的方法。
学习显式动画,可以揭示 iOS 动画库实现的整个细节,他的设计思想与实现。
1 UI 系统中各种动画的实现与 iOS 的具体实现
据我的总结,一般 UI 系统都支持如下几种动画:
第一种是帧动画 (Frame Animation)。这是最简单的动画,就是把动画拆成一帧帧图片连续播放,从而形成的动画。我们在 Image Animation 已经学习过了;
第二种是属性动画 (Property Animation)。这是最常见的动画方式,我们在 Animation 概述 中介绍过,就是把动画的本质定义为视图某些属性随时间的变化。这个也是我们今天学习的主要动画方式。
第三种是布局动画 (Layout Animation),就是当布局时其子视图展示出来的动画,比如你在桌面移动图标时,你移动一个图标,后面的图标会以动画的形式后移让开一个空。这种动画其实就是基于布局事件与属性动画复合处理而成,并不是新的动画系统。
第四种是转场动画 (Transition Animation),通常用于一个视图到另一个视图的转换。比如 A 视图渐隐 B 视图渐现这样,或 B 视图从边上推进来盖住 A 视图,其实也是属性动画的复合实现。
第五种就是视频动画。其实就是播放视频代替动画,这个就不多说了
当然因为本人对游戏编程,对 unity3d 等技术不熟,还有一些物理引擎动画,通过模拟现实生活中的物理动作实现的动画等,因为我对此不熟,就不介绍了,等我后面学习到再作个补充。
本章主要介绍显示属性动画,这也是 Core Animation 动画的核心。
1.1 让我们来设计一套属性动画系统
如果让我们来设计一套简单的属性动画系统,根据我们在 动画概述 里学习到的,主要要有这几个部分组成:
计算
- 对属性的计算,比如某时刻下可动画属性的值;计时
- 对象属性随时间是个怎样的变化;UI 刷新
- 属性变化后怎样刷新 UI;线程管理
- 当然,总不可以在主线程绘制动画吧,驱动动画的子线程我们还要加以管理。
由此,我们可以作出最简单的动画系统:
我们看下这套系统:
XXLayer
负责 UI 的显示与重绘,我们假设动画针对 layer 的一个属性propertyX
,则它的重绘方法会依赖propertyX
绘制出当前的效果;XXAnimationThread
与XXAnimation
负责动画线程管理,我们用XXAnimation
封装一个动画,它包括了一个属性动画需要的基本参数,表示在duration
时间段时,属性值从from
值过渡到to
值。XXAnimationThread
提供线程管理与计时功能,开启动画后,每隔一段时间利用XXAnimation
的参数与计时方法算出属性当前值,并调用XXLayer
的redraw
方法刷新界面;- 而
XXAnimation
中的timingFunction
负责最核心的计算功能,其
我们用伪代码剖析一下最简单动画系统的实现,主要细节在于 XXAnimationThread
的 startAnimation
方法:
1 |
1 |
|
2 转场动画 (Transition Animation)
在之前介绍 View Animation 时我们介绍过过 转场动画
。
2.1 图层树的转场动画
CATransition
可以对图层树做动画,这个是非常强大的(个人猜测这个应该是对整个图层树的 cache image 做动画实现的)。这意味着你可以不用考虑图层树的细节,直接在 layer 根部使用 transition animation
,方便地启用动画。
比如我们下面的操作想在 tab 切换时展示页面转换的渐隐渐现动画:
1 | - (void)tabBarController:(UITabBarController *)tabBarController didSelectVie |
2.2 CAMediaTiming 协议
CAMediaTiming
协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer
和 CAAnimation
都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。
- 一个动画迭代
CAMediaTiming
中定义的一些属性 (也可以称之为参数),给要进行的动画的一次迭代指定了时间与计时的参数。比如 duration
与 repeatCount
这两个参数,duration
表示动画一个迭代的持续时间, repeatCount
表示整个完整动画包含多少次迭代。,如果 duration=2
,repeatCount=3
,就意思着整个动画时长为 2*3=6
秒。
duration
与 repeatCount
的默认值为 0,分别代码 0.25 秒
与 1 次
迭代。把 repeatCount
设为 INFINITY
表示无限迭代;
- timeOffset 和 beginTime
timeOffset
和 beginTime
类似,但是和增加 beginTime
导致的延迟动画不同,增加 timeOffset
只是让动画快进到某一点,例如,对于一个持续 1 秒的动画 来说,设置 timeOffset
为 0.5 意味着动画将从一半的地方开始。
- 其它参数
speed
是一个时间的倍数,默认 1.0,减少它会减慢图层 / 动画的时间,增加它会加快速度。如果 2.0 的速度,那么对于一个 duration
为 1 的动画,实际上在 0.5 秒的时候就已经完成了。
此外,还有 autoreverse
,会自动生成从 to
到 from
的反向动画等参数,具体罗列在文章头部的 UML 图中。
最后值得一说的就是 fillMode
,它是一个 NSString 类型,表示当动画播放完后保持在一个什么状态。我们都之前说过,一般来说,动画播放完就被 remove 掉了。但是如果你设置了 removeOnCompletion
为 NO
,就会保持在动画的一个状态,而 fillMode
就是控制它展示什么状态。其可选属性:
1 | kCAFillModeForwards |
kCAFillModeRemoved
为默认值,表示动画不再播放时显示图层当前模型中的值。而 forwards,backwards,both
表示展示为动画开始时或结束时的状态。这样可以避免动画被中断,或因各种原因在结束时突然地变到一个状态去,要注意的一点就是,需要把 removeOnCompletion
设置为 NO
。
3 引用
【1】[Matt Neuburg - 《Programming iOS deep into views,view controllers and frameworks》]
【2】[Nick LockWood - 《iOS Core Animation: Advanced Techniques》]
CAAnimation
定义了动画的基本属性,在上面的类图中我用空行大致分了段,有 + (instancetype) animation
这样的方便创建动画对象的实例方法,设置动画回调对象 delegate
;有与动画计时相关的 beginTime
,timeOffset
,duration
等,还可以指定计时方法 timingFunction
,以及动画的额外控制 autoreverses
、repeatCount
等。另外 CAAnimation
实现了 KVC
,也就是你可以像字典一样对特定的 key 赋值,这点后面会说到。
CAPropertyAnimation
更进一步,对动画所对应的动画属性做了定义,keyPath
对应于要产生动画的 CALayer 动画属性。additive
表示动画结束后的属性值将做为 layer 的当前值 (即把 layer 的当前呈现值赋给模型值,参考 动画基础中的 ‘ 呈现与模型 ‘ 小节理解下)。
CABasicAnimation
则是动画继承结构中较常用到的实现类了,使用它可以定义一个具体的基本的动画行为。比如 fromValue
、 toValue
表示一个可动画的属性从一个值渐变到另一个值。byValue
表示从动画属性值的变化量,比如 fromeValue=0
,byValue=5
, 则表示可动画属性从 0 变化到 5. 而如果只设置了 byValue
,则表示可动画属性是从属性的当前值变化到 当前值 + byValue
。到这里你就知道了,使用 byValue
, 其实就是让系统帮你计算 toValue
,动画的本质还是属性值从 fromValue
变化到 toValue
。
3.1 使用 CABasicAnimation
使用 CABasicAnimation 时首先要注意下 呈现与模型问题。因为我们直接修改 layer 的值时改的是模型层的值,直到渲染周期来临时执行动画逻辑此时呈现层属性值才会从 fromValue
变到 toValue
。而动画结果后,动画对象会被干掉,此时呈现层会呈现模型层当前的属性值。也就是如果你修改的 layer 的值与动画 toValue
不一致的话,动画结果后 layer 会突然变到你改变的值,这样对用户来说很不友好。所以,一般我们需要保证动画结束后的值与当前模型层的值相等。
一种方法是省去 fromValue
和 toValue
值,因为系统会自动帮你计算。参考 动画基础中的 ‘ 呈现与模型 ‘ 小节理解下,我们说 Layer 有 呈现层 (presentationLayer)
与 模型层 (moduleLayer)
,当我们改变 layer 的值时,先是改变其模型层的值,直到下一次渲染周期到来时触发动画才开始变化呈现层的属性值。而这时呈现层的当前值也就是你改变 Layer 属性值之前的值,也就是动画的 fromValue
。而要变化到的值也就是 toValue
则是模型层当前的值。如果这样说不好理解可以参考下下面的示例(假设 animLayer 当前 x 值为 100):
1 | [CATransaction setDisableActions:YES]; // 去掉隐式动画 |
上面动画开启后,animLayer 将会从 x=100 处移动 x=200,然后停在那里。
还有另一种方法是使用 addtive
,这样就可以省去在添加动画之前修改 CALayer 模型层的属性值。因为如上文所述,动画结束后会将 呈现层 (presentationLayer)
的属性值同步到 模型层 (moduleLayer)
上。比如同样是实现上面的 layer 从 0 移动 200 处的动画
1 |