1. 动画过程中frame的变化
打印动画过程视图的frame,并且增加定时器打印视图的frame
打印frame是动画结束后view和layer的frame,也就是说,执行动画后,view和layer的frame一次性修改到最终值
2. 关于modelLayer和presentionLayer
打开CALayer头文件,发现以下两个属性:
/* Returns a copy of the layer containing all properties as they were
* at the start of the current transaction, with any active animations
* applied. This gives a close approximation to the version of the layer
* that is currently displayed. Returns nil if the layer has not yet
* been committed.
*
* 返回一个层的副本,包含在当前事务开始时的所有属性,并应用任何活动动画。这与当前显示的层的版本非常接近。如果该层尚未提交,则返回nil。
*
* The effect of attempting to modify the returned layer in any way is
* undefined.
*
* 试图以任何方式修改返回层的效果是未定义的。
*
* The `sublayers', `mask' and `superlayer' properties of the returned
* layer return the presentation versions of these properties. This
* carries through to read-only layer methods. E.g., calling -hitTest:
* on the result of the -presentationLayer will query the presentation
* values of the layer tree.
*
* 返回层的“子层”、“掩码”和“超层”属性返回这些属性的表示版本。这贯穿到只读层方法。例如,在-presentationLayer的结果上调用-hitTest:将查询层树的表示值。
*/
- (nullable instancetype)presentationLayer;
/* When called on the result of the -presentationLayer method, returns
* the underlying layer with the current model values. When called on a
* non-presentation layer, returns the receiver. The result of calling
* this method after the transaction that produced the presentation
* layer has completed is undefined.
*
* 当调用-presentationLayer方法的结果时,返回具有当前模型值的底层。在非表示层上调用时,返回接收方。在生成表示层的事务完成后调用该方法的结果是未定义的。
*/
- (instancetype)modelLayer;
这时我们打印presentationLayer,结果如下:
打印结果证明presentationLayer才是动画执行时变化的视图。
动画的整个过程其实经历了三个树状结构,才显示到屏幕上:模型树-->呈现树-->渲染树
通常我们操作的是模型树。
在重绘周期后,我们会将模型树相关内容(层次结构、图形属性和动画)序列化,通过IPC传递给专门负责屏幕渲染的渲染进程。渲染进程拿到数据并反序列化出树状结构--呈现树。
这个呈现树图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
我们可以通过CALayer的presentationLayer来访问对应的呈现树图层。
注意:呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。
-modelLayer方法:在呈现树图层上调用-modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回-self(实际上我们已经创建的原始图层就是一种数据模型)。
一个移动的图层是如何通过数据模型呈现的:
大多数情况下,你不需要访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。
两种情况下presentationLayer呈现图层会变得很有用:
- 同步动画
- 动画过程中处理用户交互
当模型树上带有动画特征是,提交到渲染进程后,渲染进程会根据动画特征,不断修改呈现树上的图层属性,并同时不断的在屏幕上渲染出来,这样我们就看到了动画。
3. 模型树与呈现树的关系比喻
在CALayer内部,它控制着两个属性presentationLayer(以下简称P)和modelLayer(以下简称M)。
P值负责显示,M只负责数据的存储和获取。
我们对layer的各种属性赋值比如frame,实际上是直接对M赋值
而P将在每一次屏幕刷新的时候回到M的状态。
比如此时M的状态是1,P的状态也是1,然后我们把M的状态改为2,那么此时P还没有过去,也就是我们看到的状态P还是1,在下一次屏幕刷新的时候,P才变为2.而我们几乎感知不到两次屏幕刷新之间的间隙,所以感觉就是我们一对M赋值,P就过去了。
P就像个瞎子,M就像是瘸子,瞎子背着瘸子,瞎子每走一步(也就是屏幕每次刷新的时候)都要去问瘸子应该怎样走(这里的走路就是绘制内容到屏幕上),瘸子没法走,只能指挥瞎子背着自己走。
重点:动画完成回到原地
可以简单的理解为:一般情况下,任何时刻P都会回到M的状态。
而当一个CAAnumation(以下简称A)加到layer上面后,A就把M从P身上挤下去了,现在P背着的是A,P同样在每次屏幕刷新的时候去问他背着的那个家伙,A就指挥它从fromValue到toValue来改变值。而动画结束后,A会自动被移除,只是P没有了指挥,就只能大喊“M你在哪”,M说我还在原地没动呢,于是P就顺声回到了M的位置。
这就是为什么动画结束后我们看到这个视图又回到了原来的位置,是因为我们看到在移动的是P,而指挥它移动的是A,M永远停在原来的位置没有动,动画结束后A被移除,P就回到了M的怀里。
动画结束后,P会回到M的状态(当然这是有前提的,因为动画已经被移除了,我们可以设置fillMode来继续影响P),但是这通常不是我们动画想要的效果。我们通常想要的是,动画结束后,视图就停留在结束的地方,并且此时我们去访问该视图的属性(也就是M的属性),也应该就是当前看到的样子。按照官方文档的描述,我们的CAAnimation动画都可以通过设置modelLayer到动画结束的状态来实现P和M的同步。
4. 动画的实现方式
iOS中,实现动画的方式主要分两大类:
- CoreAnimation动画
- 非CoreAnimation动画
CoreAnimation动画:
CoreAnimation动画,即基于事务的动画,是最常见的动画实现方式。动画执行者是专门负责渲染的渲染进程,操作的是呈现树presentationLayer。我们应该尽量使用CoreAnimation来控制动画,因为CoreAnimation是充分优化过的:
- 更高效的绘制
基于Layer的绘图过程中,CoreAnimation通过应该操作位图(变换、组合灯),产生的动画速度比软件操作的方式快很多。
基于View的绘图过程中,view被改动时会触发drawRect:方法来重新绘制位图,但是这种方式需要CPU在主线程执行,比较耗时。而CoreAnimation则尽可能的操作硬件中已缓存的位图,来实现相同的效果,从而减少了资源损耗。 - 更高效的动画
在动画过程中,CoreAnimation会通过硬件来一帧一帧的绘制。你所做的就是指定动画的起点和终点,其他都让CoreAnimation来做。当然你也可以自定义动画参数,否则CoreAnimation会使用合适的默认值。
非CoreAnimation:
非CoreAnimation动画执行者是单签进程,操作的是模型树modelLayer。常见的有定时器动画和手势动画。定时器动画是在定时周期触发时修改模型树的图层属性;手动动画是手势事件(比如UIScrollView的didScroll)触发时修改模型树的图层属性。两者都能达到视图随着时间不断变化的效果,即实现了动画。
非CoreAnimation动画过程中实际上不断改动的是模型树,而呈现树紧紧成了模型树的复制品,状态与模型树保持一致。整个过程中,主要是CPU在主线程不断的调整图层属性、布局计算、提交数据,没有充分利用到CoreAnimation强大的动画控制功能。
以上部分关于layer的描述摘自文章链接
5. 动画过程中的点击交互
如果你想让你做动画的图层响应用户事件:
你可以使用-hitTest:方法来判断指定图层是否被触摸,这时候对呈现树图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.button = [UIButton buttonWithType:UIButtonTypeCustom];
self.button.frame = CGRectMake(0, 100, 50, 50);
self.button.backgroundColor = [UIColor redColor];
self.button.userInteractionEnabled = NO;
[self.view addSubview:self.button];
[UIView animateWithDuration:5 animations:^{
self.button.frame = CGRectMake(300, 500, 50, 50);
// NSLog(@"animate view frame %@", NSStringFromCGRect(button.frame));
// NSLog(@"animate layer frame %@", NSStringFromCGRect(button.layer.frame));
// NSLog(@"animate presentationLayer frame %@", NSStringFromCGRect(button.layer.presentationLayer.frame));
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
// // 方案一
// if (CGRectContainsPoint(self.button.layer.presentationLayer.frame, point)) {
// [self btnClick];
// }
// 方案二
if ([self.button.layer.presentationLayer hitTest:point] != nil) {
[self btnClick];
}
}
- (void)btnClick
{
NSLog(@"%s", __func__);
}
方案一:直接使用看了CGRectContainsPoint(CGRect rect, CGPoint point)方法,该方法返回一个BOOL值,判断point是否在rect内部,刚好可以穿给presentationLayer的frame和当前点击的point。
方案二:直接使用了hitTest:(CGPoint)p方法,该方法返回一个CALayer对象,如果点击的point在其内部,返回一个layer对象。
这里还要注意一点,使用button时,需要将userInteractionEnabled设置为NO。
6. 手势识别与响应者链
从图中我们可以看到,在通过命中测试找到第一响应者之后,会将 UITouch 分发给 UIResponder 的 touches 系列方法,同时也会分发给手势识别系统,让这两个处理系统同时工作。
首先要注意的是,上图中蓝色部分的流程并不会只执行一次,举例来说:当我们用一根手指在一个视图上缓慢滑动时,会产生一个 UITouch 对象,这个 UITouch 对象会随着你手指的滑动,不断的更新自身,同时也不断地触发 touches 系列方法。一般来说,我们会得到如下类似的触发顺序:
touchesBegan // 手指触摸屏幕
touchesMoved // 手指在屏幕上移动
touchesMoved // ...
...
touchesMoved // ...
touchesMoved // 手指在屏幕上移动
touchesEnded // 手指离开屏幕
UITouch 的 gestureRecognizers 属性中的存储了在寻找第一响应者的过程中收集到的手势,而在不断触发 touches 系列方法的过程中,手势识别系统也在在不停的判断当前这个 UITouch 是否符合收集到的某个手势。
当手势识别成功: 被触摸的那个视图,也就是第一响应者会收到 touchesCancelled 的消息,并且该视图不会再收到来自该 UITouch 的 touches 事件。同时也让该 UITouch 关联的其他手势也收到 touchesCancelled,并且之后不再收到此 UITouch 的 touches 事件。这样做就实现了该识别到的手势能够独占该 UITouch。具体表现参考如下:
touchesBegan // 手指触摸屏幕
touchesMoved // 手指在屏幕上移动
touchesMoved // ...
...
touchesMoved // ...
touchesMoved // 手指在屏幕上移动
touchesCancelled // 手势识别成功,touches 系列方法被阻断
// 现在手指💅并没有离开屏幕
// 但如果继续滑动🛹的话
// 并不会触发 touches 系列方法
当手势识别未成功: 指暂时未识别出来,不代表以后不会识别成功,不会阻断响应链。注意这里指的是未成功,并不一定是失败。在手势的内部状态中,手势大部分情况下状态是 .possible,指的是 UITouch 暂时与其不匹配,但之后可能有机会识别成功。而 .fail 是真的识别失败,指的是以目前的触摸情况来看已经不可能是这个手势了,并且在下个 runloop 会从 gestureRecognizers 中移除该手势。
下面举个简单的例子模拟一下响应链和手势的相互影响。现在用一根手指,在一个视图上触摸并滑动一段距离。下图给出了视图不带手势的情况,和带一个 UIPanGestureRecognizer 手势的情况。
从图中我们可以看到,当不带手势的情况下,手指按下去的时候,响应者的 touchBegan 方法会触发,随着手指的移动,touchMoved 会不断触发,当手指结束移动并抬起来的时候,touchEnded 会触发。在这个过程中,我们接收到一直是一个不断更新的 UITouch。
在该视图有添加一个 UIPanGestureRecognizer 手势的情况下,我们多了下方这一条来表示与响应链同时工作的手势识别系统,可以看到手势识别系统也是在手指按下去那一刻就开始工作的,前半段处于一直正在识别的状态。在我们拖动了很小一段距离之后(注意这时候我们的手指还没抬起), 手势识别系统确定了该 UITouch 所做的动作是符合 UIPanGestureRecognizer 的特点的,于是给该视图的响应链发送了 touchCancelled 的信息,从而阻止这个 UITouch 继续触发这个视图的 touches 系列方法(同时也取消了别的相关手势的 touches 系列方法,图中未体现)。在这之后,被调用的只有与手势关联的 target-action 方法(也就是图中的墨绿色节点 call PanFunction)。
为了图片的美观和易读,在图片中我隐去了不少细节,在此列出:
- 手势识别器的状态在图中未标出:
- 手势在图中 recognizing 的橙色节点处和 recognized 棕色节点处都处于 .possible 状态
- 手势在图中绿色节点处的状态变化是 .began -> [.changed] -> ended
- 手势识别器不是响应者,但也有 touches 系列方法,比它所添加的视图的 touches 方法更早那么一点触发
- 从图中也可以看出,手势那条线上的每个节点都稍靠左一些
- 手势那条线上的橙、棕、墨绿色节点处也可以看做手势识别器的 touches 方法触发
- 更详细的触发顺序应当如下图所示(在一个 UIView 上添加了 UIPanGestureRecognizer ,并单指在上面滑动一段距离的情况)