写在前面
在体验至上的时代,一些自然酷炫流畅的动画无疑能提高用户对app好感度以及留存率。常见动画有如下拉加载、加载进度以及转场动画等,iOS开发中的动画大家都不陌生。Core Animation框架提供了一套CALayer层动画接口。因为CALayer作为UIView的backing layer的关系,UIKit基于CA框架也提供了一套视图动画接口,归根到底还是CA动画。
这篇文章,主要是来聊聊动画的一些基本概念,不是来解析具体的动画效果实现。不妨先停下来想一想,动画是什么,它的基本实现原理是什么,我们能做什么。我们从框架提供的接口的角度来认识动画。
动画是什么:CAAnimation
CAAnimation
是Core Animation框架中的一个基类,但并不实际用于动画实现,只是定义了“动画”对象所遵循的基本接口,具体动画效果通过子类提供。其子类有如:CAPropertyAnimation
属性动画及其两个子类CABasicAnimation
、CAKeyFrameAnimation
,iOS9推出的CASpringAnimation
、CATransition
(都是继承CABasicAnimation
)以及动画组CAAnimationGroup
(继承于CAAnimation
)。我们主要基于基本动画进行分析,动画组及帧动画也是基本动画的组合。
CAAnimation
遵循两个关键的协议:CAAction
以及CAMediaTiming
。
CAAction
是一个事件处理的接口(动画行为),要求实现接口的对象实现-(void)runActionForKey:(NSString *)event object:(id)anObject arguments:(nullable NSDictionary *)dict
进行事件处理(也就是执行动画)。通常而言,object是layer,event则是key,每个layer保存了一份从event到action的映射表(可参考接口- (void)addAnimation:(CAAnimation *)anim forKey:(nullable NSString *)key
)。对于系统支持的隐式动画,每当layer的(可动画)属性被修改时,会触发相同名称(属性名)的事件,系统有相应的CAAnimation
对象与该事件绑定。另外,系统提供了两个非直接关联到属性的事件:onOrderIn
及 onOrderOut
,当layer可见或非可见时触发。
CAMediaTiming
是一个关键的协议,也是本文所关注的点。CALayer
及CAAnimation
都遵循实现了该协议。CAMediaTiming
对分层时间系统进行了建模,每个对象描述了其父对象时间(parent time)到本身时间(local time)的映射,local time包含两个:active local time及basic local time。时间的转换包含了两个阶段:一是,从父对象时间(parent time)转换为本地活跃时间(active local time),包含了本对象出现在父对象时间线上的点以及其相对(父对象)时间速度;二是,从active local time到basic local time,允许对象本身时长的周期性重复以及逆向播放。
除此之外,CAAnimation
定义了另外一个关键属性:timingFunction
,类型是CAMediaTimingFunction
,这是一个动画时间控制函数。
看完了基本接口,回想一下最初提出的问题,“动画是什么?”。举些大家日常都比较熟悉的例子:电影慢镜头及GIF。电影慢镜头可以将一些短时间内发生的变化场景捕获后,以更长的时间来展示;GIF简单来说,将很多张图片进行合并到同一个图片文件就可以制作出一个GIF,每张图片可以控制周期内播放时长。这些都跟“时间”紧密相关,我们对场景变化的感知是基于时间的。动画,是一定时间内场景(状态)的变化所创造的效果。
CAMediaTiming
就如Core Animation框架的时间系统。我们接触的基本动画、交互动画以及物理模拟动画,其实都跟这个概念密切相关。
时间系统:CAMediaTiming
CAMediaTiming
提供了8个属性,分别是:
duration
:“The basic duration of the object. Defaults to 0.”(API的解释,下同),这个不一定是真实的动画时长,真实的还跟速度speed
以及上层对象时间体系有关。speed
:“The rate of the layer. Used to scale parent time to local time, e.g. if rate is 2, local time progresses twice as fast as parent time. Defaults to 1.”,当前层的速率,用于将父对象时间拉伸到本身时间。beginTime
:“The begin time of the object, in relation to its parent object, if applicable. Defaults to 0.”,本对象在父对象的时间线中开始的位置,比如本身一个anmation添加到一个animation group中时,beginTime设定了动画组开始后当前动画的开始时间(也即是延迟)。但对于将animation添加到layer中时,layer作为parent object它的开始时间是过去时间不能这样使用,需要明确确定其beginTime可以选择对当前绝对时间的偏移。当前绝对时间可通过CACurrentMediaTime()
获取并[layer convertTime:CACurrentMediaTime() fromLayer:nil]
转换到layer的时间坐标系中。timeoffset
:“Additional offset in active local time. i.e. to convert from parent time tp to active local time t:t = (tp - begin) * speed + offset
. One use of this is to “pause” a layer by settingspeed
to zero andoffset
to a suitable value. Defaults to 0.”,这里给出了一条时间转换关系公式,对于父对象中的时间点tp,在本身时间坐标系统时间为t = (tp - begin) * speed + offset
,也就是父时间进行伸缩并加上偏移就得到当前对象的时间点。不是很直观理解,举个例子:动画组anigroup(作为parent object)时长3秒分别对应状态p1、p2、p3,一个动画anim时长6秒分别对应状态a1~a6,其动画速度speed
为2、偏移offset
为1,被添加到动画组anigroup中;那么当动画组anigroup状态为p2时,动画anim的状态为a5。fillmode
、repeatCount
,repeatDuration
及autoreverses
动画重复及逆向播放的控制属性。
如上面提到的,动画是一定的时间规律下的状态切换,这包含两层含义:一是时间变化规律,二是状态切换。动画效果需要分别对这二者进行控制(或者可以认为是动画状态变化的两面体现,动画既包括值的变化也包括时间的变化;动画的插值与时间相关,时间变化规律指的也是插值的变化,否则感知上时间就是线性变化的)。不过实际上,开发中通常是指定动画的状态切换效果,其时间规律通常是采用已有的动画时间控制函数。除此之外,时基动画需要对时间进度进行管理,上面所列的公式t = (tp - begin) * speed + offset
不是用来直接确定动画状态切换的时间规律(效果)的,而是用于确定父对象时间到本对象时间映射(进度)的。其中三组控制变量:父时间进度(tp-begin)、速度(speed)、偏移(offset)。
举几个例子说明时间的进度控制:
- Xcode模拟器的动画调试慢放,通过修改动画播放的
speed
; - 动画暂停,将
speed
置为0,并指定相应的offset
;甚至还可以通过定时器控制offset的方式,来实现逆向播放等。
插值与时间控制函数:CAMediaTimingFunction
时基系统的动画,按时间的步进更新动画对象的状态,这涉及了动画状态的插值问题。
比如说一个位置平移动画(比如说弹簧效果),仅知道动画时间duration
、始末状态valueBegin
、valueEnd
是不够的,动画的过程需要知道动画时间t
时的位置状态valueT
。实现这个具体的动画也很简单,我们需要定义一个以时间t为控制变量的插值函数value=mapfunc(t, duration, valueBegin, valueEnd)
,随着时间t的步进对location
进行更新即可。
但上述的插值函数mapfunc
的问题在于,它是与具体特定动画的动画时间duration
、始末状态值相关的。即使是同一效果,不同振幅或不同动画时间都需要提供一个相应的插值函数!不具备可重用性。那假如把动画定义为一种“效果”(比如,弹簧震荡效果),我们如何将这种效果抽象出来?
CAMediaTimingFunction
登场。那CAMediaTimingFunction
做了什么?CAMediaTimingFunction
能将某种“效果”的插值过程抽象出来吗?最初的需求,动画状态的插值还是要实现的。
回到问题的本质,动画的状态切换是基于时间的插值,其实是一个映射问题,value=mapfunc(t, duration, valueBegin, valueEnd)
是从线性变化变量t到value的一个非线性变换。为了将动画效果与具体动画状态区分开来,我们需要将时间参数t与其他参数分离,分别构造两个映射函数:
- mapvalue(tProgress, duration, valueBegin, valueEnd)
- tProgress = maptime(tReal)
将状态的切换转换为基于进度的插值,tProgress
是一个进度值,mapvalue
进行线性映射;另外增加一个时间映射函数maptime
,将(单位)动画时间映射为状态切换进度tProgress,动画效果将时间控制部分分离了。这正是CAMediaTimingFunction
的职责:时间控制(时间映射)。
CAMediaTimingFunction
做了动画时间到动画进度(映射到始末状态的线性插值)的映射,其中系统预定义了五种时间控制方法,另外支持定义三次贝塞尔曲线的控制函数。
然后我们回到UI及动画的渲染过程,结合上面所说的时间控制理解一下UI渲染的过程。
####CALayer的渲染及动画
图层树
CALayer拥有与UIView一样的树状层次结构,实际上作为UIView的backing layer是用做内容显示的管理,而UIView则主要添加了事件机制。对应UIView树形结构,CALayer有它的图层树(模型树)、呈现树及渲染树,对应每个CALayer的modelLayer
,presentationLayer
及renderLayer
。modelLayer
是我们代码层面反应对layer的属性的修改,presentationLayer
则是动画过程中与界面展示一致的图层数据,renderLayer
则是框架私有的(见上图)。可参考Apple官网的图示:
作为UIView的layer也被称为Root Layer,其代理指定为关联的UIView,用于管理layer的绘制/渲染,代理事件主要包括:
1 | @protocol CALayerDelegate <NSObject> |
界面的渲染过程在objc.io有一期介绍。下面是渲染的基本过程图解:
总的来说,每次运行循环CPU按需要创建好UIView的缓冲区,对view内容进行绘制(CPU工作),然后缓冲区交给GPU渲染,最后实际显示到屏幕上:
每个UIView的backing layer有一个
content
字段,指向的是一块位图缓冲区,称为backing store(可以理解成CGImageRef);CPU对UIView的绘制,主要在动画事务(下文详细介绍)提交、完成布局工作后,
[CALayer display]
触发的各层事件回调([CALayer drawInContext:]
、[UIView(CALayerDelegate) drawLayer:inContext:]
、[UIView drawRect:]
)中在CPU层面对界面的绘制,数据通过CGContextRef写入backing store;
backing store写完后,通过渲染服务交给GPU进行渲染,将backing store中的位图数据进行显示。
每当修改视图或层的属性,将导致位图缓冲区的重新创建,重新绘制后,layer的content
字段会重新指向新的位图缓冲区。CPU影响的是布局与CPU层面的渲染。
GPU是基于OpenGL对Texture纹理数据进行处理的,Core Animation框架可以创建Texture并将其与位图数据绑定。整个过程如下图示:
GPU处理需要将位图数据从RAM搬到VRAM,主要是处理Texture的渲染工作,包括:
- 纹理组合,相当于UIView的层次叠加处理,公式
R = S+D*(1-Sa)
,图层的透明度有重大影响; - 像素缩放采样,位图数据与最终渲染的数据大小不一致需调整
- 离屏渲染
动画事务:CATransaction
Core Animation假设屏幕上所有东西均可进行动画,对CALayer的可动画属性进行修改时,默认会导致动画的发生来保证平滑过渡,这便是隐式动画。之所以称之为隐式,是因为没有指定它的动画时间、类型等。实际上,动画的执行时间取决于当前事务的设置,动画类型取决于图层行为。
动画事务CATransaction
是Core Animation用来包含一系列属性动画集合的机制,用于将多个可动画属性的修改集中提交执行。任何用指定事务来改变图层的可动画属性时,不会立即发生变化而是等事务提交后才有动画过渡,所有动画属性的修改都应处于事务中。CATransaction
分为显式及隐式事务,显式的事务使用如下:
1 | [CATransaction begin]; |
动画事务可嵌套,嵌套的动画事务,只有最外层事务提交时才执行整个动画。CATransaction
没有实例属性,对duration及是否动画等配置的修改会影响当前事务。对于想无动画设置属性可以设置kCATransactionDisableActions
的key或+setDisableActions
(停止了动画action查询)。
Core Animation在每个Runloop周期中自动开始一次新的事务,任何一次Runloop中的动画属性的修改都被集中起来,做一次0.25s(默认)的动画。这也是隐式动画的所使用事务。只有非Root Layer的可动画属性修改会触发隐式动画,因为Root Layer有关联的UIView作为其delegate,在查询对应属性key的action时返回了nil。
UIView中的类方法+beginAnimations:context:
及+commitAnimations
以及基于block的动画接口中,都有添加了事务。
动画的创建与执行
iOS显示系统是vSync信号驱动,硬件中断信号之后,会通知到runloop,在新的一轮runloop循环中CPU完成好界面布局及绘制。对于动画,首先是上面所提的CATransaction
事务会将我们认为的“动画”行为提交到Core Animation,Core Animation在runloop中注册了事件状态监听,并在回调中,将中间状态提交到GPU处理。硬件时钟的回调基本是线性时间的,这也是为什么需要“时间控制函数”的原因。另外GPU或CPU层面的处理耗时过长会导致直接丢弃当前帧渲染,表现出来就是界面卡顿。
篇幅过长,有部分简单带过,有时间再补上。
Author: Jason
Permalink: http://blog.knpc21.com/ios/ios-animation-mediatiming/
文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。
Comments