[iOS] 从隐式动画开始看动画
其实之前有写过《高级动画》的阅读笔记,主要是关于view的,这次其实也是啦,只是把之前零零碎碎说过的几个知识点都汇总汇总,顺带探讨点别的。
1. 隐式动画
我们每个 UIView
都有一个默认的 rootLayer
,这个 layer
其实才是我们看到的真正的东西,UIView
反而是持有了一个 layer
的壳子,这样方便多平台共享 layer
但可以换个壳。于是当我们修改 UIView
的颜色的时候,其实真正改的是 rootLayer
的颜色哦。
如果我们直接改 UIView
的位置,他会一下子就变过去,大家应该都有尝试过~ 然鹅,如果你给 UIView
再加一个layer,类似酱紫:
CALayer *yellowLayer = [[CALayer alloc] init];
yellowLayer.frame = CGRectMake(50, 220, 100, 100);
yellowLayer.backgroundColor = [UIColor yellowColor].CGColor;
yellowLayer.delegate = self;
[testView.layer addSublayer:yellowLayer];
然后你再去改变这个新加 layer 的属性会发现一个很神奇的事情,他会加个默认的动画:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
yellowLayer.frame = CGRectMake(50, 320, 100, 100);
});
隐式动画
是不是很神奇,这个就叫隐式动画啦~
当对非Root Layer的部分属性进行修改时,默认会自动产生一些动画效果,而这些属性称为Animatable Properties(可动画属性)
列举几个常见的Animatable Properties:
- bounds:用于设置CALayer的宽度和高度。修改这个属性会产生缩放动画。
- backgroundColor:用于设置CALayer的背景色。修改这个属性会产生背景色的渐变动画。
- position:用于设置CALayer的位置。修改这个属性会产生平移动画。
如果我不想有这个动画肿么破呢?
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
yellowLayer.frame = CGRectMake(50, 320, 100, 100);
[CATransaction commit];
});
把代码改成上面的样子,你就会发现小黄嗖的一下就挪下去了,看不到他的移动轨迹~ 毕竟其实改动属性的时候就是自动加了个动画,用 CATransaction
(可以refer:https://www.jianshu.com/p/a9a2e1e3d07a
) 包一下然后指明禁用隐式动画,在本次Transaction中就不会有啦。
下一个问题是,为啥会有隐式动画?
给UIView做动画其实操控的是 CALayer,我们先看看它的属性:
截屏2021-09-04 上午8.30.32.png
这些属性后面注释的 Animatable
是做啥的呢?
如果一个属性被标记为Animatable,那么它具有以下两个特点:
- 直接对它赋值可能产生隐式动画;
- 我们的CAAnimation的keyPath可以设置为这个属性的名字。
再一个问题是,隐式动画是怎么实现的呢?那我们先看看动画的实现叭
2. CALayerDelegate 的 -actionForLayer:forKey:
在做啥
我们都知道动画可以通过很多种方式加,比如CAAnimation
/ UIView animate
,还有一些不常用的,比如Block动画
、UIImageView的帧动画
、UIActivityIndicatorView
。
我们给 View 做动画改变它的属性,其实最后改的都是 layer。
通过苹果我们知道,决定 layer 如何做动画的是 delegate 的actionForLayer
方法。
/* If defined, called by the default implementation of the
* -actionForKey: method. Should return an object implementing the
* CAAction protocol. May return 'nil' if the delegate doesn't specify
* a behavior for the current event. Returning the null object (i.e.
* '[NSNull null]') explicitly forces no further search. (I.e. the
* +defaultActionForKey: method will not be called.) */
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
也就是说,如果 actionForLayer
返回 nil,就不会有啥特殊的(也就是会走隐式动画),如果返回[NSNull null]
就会阻断动画啦。
我们先尝试给一个自定义 View 加个动画~ 这里用自定义view是为了复写方法打印哈:
@interface TestView : UIView <CALayerDelegate>
@end
@implementation TestView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
NSLog(@"action : %@", action);
return action;
}
然后我们去给这个自定义view做个动画:
self.view1 = [[TestView alloc] initWithFrame:CGRectMake(0, 0, 400, 800)];
[self.view addSubview:self.view1];
// just a demo, ignore the weak strong
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:3 animations:^{
self.view1.alpha = 0.5;
}];
});
然后我们打个断点给自定义view的actionForLayer
:
是不是很神奇,我们虽然调用的是 UIView animateWithDuration
,但是实际上却发现,action返回的是CABasicAnimation
,也就是说,其实UIView animate
是通过 CAAnimation 实现的哦!
3. addAnimation
和 actions 有关系么
我们再试试给他加一个 CAAnimation 康康~
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
rotationAnimation.duration = 0.6;
rotationAnimation.repeatCount = INFINITY;
rotationAnimation.removedOnCompletion = YES;
[self.view1.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
如果酱紫的话,actionForLayer
好像并没有调用,也就是说其实它已经知道action是啥了,所以没去问 delegate,为啥呢?
看起来就是我们通过addAnimation
会把创建的 CAAction
和 key 绑定起来,但是不是放进 actions 的字典哦,因为我发现,加完动画以后打印 layer 的 actions 还是nil哦:
所以
CAAnimation
和 layer 的actions
关系不大,CAAnimation
一但被添加,layer就会动起来(如果没有设置delay),因为它已经拿到了CAAction
。但是如果是你随便改了 layer 的一个属性,却没有告诉他用什么CAAction
,那么他就需要先找 layer 要 action,而要 action 的过程就是下面酱紫的。
4. CALayer 的 actionForKey
怎么找
注意下图是 CALayer 的 actionForKey
方法,不要和它的 delegate 里面的方法弄混哦~ 当向 layer 找 action 时,他会按照下面的顺序找:
- delegate 的
-actionForLayer:forKey:
- actions 字典
- style 的 actions 字典
+defaultActionForKey:
这个时候我最感兴趣的是,actions 这个字典好像 CALayer 没有提供啥接口是操控它的,好像直接字典往里塞的样子,类似下面这里:
// 抄的哈
//使用actions也能够给属性添加属性设置动画
let outterCirqueAnim = CABasicAnimation.init()
outterCirqueAnim.keyPath = "circqueOuterRadius"
outterCirqueAnim.duration = 3
outterCirqueAnim.fromValue = animatorLayer!.circqueOuterRadius
animatorLayer!.actions = NSMutableDictionary.init(object: outterCirqueAnim, forKey:"circqueOuterRadius" as NSCopying) as? [String : CAAction]
(animatorLayer?.actions!["circqueOuterRadius"] as! CABasicAnimation).fromValue = animatorLayer!.circqueOuterRadius
animatorLayer!.circqueOuterRadius += 10
那么 style 的 actions 又是啥呢?这个其实就是 CALayer 有个属性是 style
,也是一个字典,但我猜测应该也是 key action 这种键值对。只是我很迷惑,为啥已经有了 actions 还要加个 style?
于是给一个 layer 加 action 有很多种方法,比如:
- 给 layer 加 delegate,然后在
actionForLayer:forKey:
返回 - 继承 CALayer 写个自定义的layer,然后复写
actionForKey:
- 直接拿 layer 的 actions / style,往里面塞键值对(这个可以不用写个新的 CALayer)
触发上面的方法的时机
- 设置属性时自动触发(本文使用的方式,例如设置圆环内径)
- CALayer实例对象刚开始可见时自动触发[设置kCAOnOrderIn属性]
- CALayer开始不可见时自动触发(设置kCAOnOrderOut属性)
- 添加到了一个CATransition(添加CATransition方法)
5. CATransaction 触发 action
CATransaction
是怎么做的呢?我们知道其实他和 UIView animate
类似,就是只是指明了要改啥,对怎么改能设定的余地不多,主要是duration可以,那么当我们使用 CATransaction
的时候,有木有触发 actionForKey
呢?
[CATransaction begin];
[CATransaction setAnimationDuration:3];
self.view1.alpha = 0.5;
[CATransaction commit];
然后我们给 View1 的 actionForLayer:forKey:
打个断点:
果然是触发了,毕竟它木有一个现成的 CAAction 绑定,于是只能去问 layer 要啦~
6. 再看隐式动画
现在我们会看隐式动画,解决一个问题,就是为啥 rootLayer 没有隐式动画?
原因是酱紫的,因为当 View 创建的时候,会自动创建 rootLayer,并将 rootLayer 的 delegate 设为 view,此时,如果你修改layer的属性,其实他会问 view 要个action:
截屏2021-09-05 下午6.19.45.png
悲伤的是,- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
返回的是NSNull
,也就是直接拒绝了动画。
然鹅非 rootLayer 在创建的时候是木有 delegate 的,于是相当于- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
返回的是 nil,没有阻断动画,然后会继续询问 actions 字典以及defultActionForKey:
。
然鹅我发现一个神奇的事情,就是如果虽然隐式动画会让 layer 的 actionForKey
返回一个 CAAction
,但是实际上这个 layer 的 delegate / actions / style 都是nil,并且defultActionForKey:
返回也是nil。那么也就是说,其实 actionForKey
可能并不是只看这四个,还有其他的逻辑。因为木有查到源码只能作罢,但隐式动画好像有点特殊~
此外,不瞒你说,我并没有发现什么key的defaultActionForKey
返回是有值的,有可能这个方法主要是用于子类覆写的... 略微难以理解,毕竟他的名字看起来就像是给隐式动画的。
7. UIView beginAnimation
会有什么神奇作用
之前看了 UIView animate
会自动产生一个 CAAction
,那么beginAnimations
是不是也是酱紫呢?
id beforeAction = [self.view.layer actionForKey:@"position"];
NSLog(@"action before:%@", beforeAction);
[UIView beginAnimations:nil context:nil];
id innerAction = [self.view.layer actionForKey:@"position"];
NSLog(@"action inner:%@", innerAction);
[UIView commitAnimations];
id outerAction = [self.view.layer actionForKey:@"position"];
NSLog(@"action outter:%@", outerAction);
打印是酱紫的:
021-09-05 22:38:55.758222+0800 Example1[4237:2284737] action before:(null)
2021-09-05 22:45:25.004690+0800 Example1[4237:2284737] action inner:<_UIViewAdditiveAnimationAction: 0x2830a50a0>
2021-09-05 22:45:25.005035+0800 Example1[4237:2284737] action outter:(null)
这个时候我就很好奇,innerAction不为空是什么返回了非nil值,难道也是有啥特殊逻辑?于是我在inner里面打了断点po了一下:
截屏2021-09-05 下午10.44.01.png
好的吧看起来如果是通过 UIView 增加动画,由于 layer 的 delegate 就是 UIView,他在动画区域内的 actionForLayer:forKey:
就会是非空啦。
8. 显示层和模型层
这个蛮推荐下面系列里面的 :https://blog.csdn.net/u013282174/article/details/50388546 最后一段的描述的~
但简单一点说是酱紫的,你看到的 layer 动画其实都是假的,为啥这么说呢,比如你给 layer 设置一个新位置,你会看到它慢慢挪过去,其实在你设置的那一刻,modelLayer
已经变到了新的位置,只是presentationLayer
也就是你看到的 layer 在慢慢挪。
https://www.jianshu.com/p/e6d44ca9c103 这篇文章里我也有写到这个部分。
也就是说,当有了 CAAction,那么 presentationLayer
的位置就由 CAAction 来确定了,他会不断地问 CAAction 自己现在应该在哪里,然后当动画结束,CAAction就木有了被移除了,那么 presentationLayer
就会去找 modelLayer
啦~
layoutIfNeeded 动画
那么当我们改变布局以后调用 layoutIfNeeded
的动画是什么原理呢?
- (void)addConsAnim {
[self.view layoutIfNeeded];
NSLog(@"view1 frame:%@", @(self.view1.frame));
self.view1BottomCons.constant = 500;
NSLog(@"view1 frame:%@", @(self.view1.frame));
[UIView animateWithDuration:3 animations:^{
[self.view layoutIfNeeded];
NSLog(@"view1 frame:%@", @(self.view1.frame));
NSLog(@"view1 modellayer frame:%@", @(self.view1.layer.modelLayer.frame));
NSLog(@"view1 presentlayer frame:%@", @(self.view1.layer.presentationLayer.frame));
}];
}
注意如果你想动画生效,及的调用父view的layoutIfNeeded
哦~
然后我们看打印结果:
2021-09-05 23:35:37.296548+0800 Example1[4565:2307272] view1 frame:NSRect: {{87, 502}, {240, 128}}
2021-09-05 23:35:37.296875+0800 Example1[4565:2307272] view1 frame:NSRect: {{87, 502}, {240, 128}}
2021-09-05 23:35:37.297507+0800 Example1[4565:2307272] view1 frame:NSRect: {{87, 234}, {240, 128}}
2021-09-05 23:35:37.297615+0800 Example1[4565:2307272] view1 modellayer frame:NSRect: {{87, 234}, {240, 128}}
2021-09-05 23:35:37.297741+0800 Example1[4565:2307272] view1 presentlayer frame:NSRect: {{87, 502}, {240, 128}}
也就是说当你在动画块里面调用layoutIfNeeded
的瞬间,view1的frame已经变了,modelLayer也变了,只有presentationLayer没有变。这也是为啥明明动画时长肯定大于一个 runloop 刷新周期(每次 runloop 会自动刷新 layout),但动画并没有因为 runloop 更新被突然放置到最终位置,因为它本身已经在最终位置了,只是 presentationLayer 米有。
然后就是它怎么实现的动画呢?我们给 view1 里面打个断点康康:
截屏2021-09-05 下午11.39.29.png
layoutIfNeeded
会触发类似setFrame
的动作,于是就会触发属性动画,也就会去找 layer 要actionForKey
了~ 于是作为delegate的view1的actionForLayer:forKey:
会被触发,并且因为在动画block内,会返回一个非空action。
如果我们hook一下delegate的 actionForKey
让它返回空:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
// id action = [super actionForLayer:layer forKey:event];
return [NSNull null];
}
改完以后layout动画就没了哦,view会直接到制定位置,没有动画。
于是我有一个小猜测,其实 runloop 周期自动更新的是 modelLayer
,只是因为没有动画的时候 presentationLayer
和 modelLayer
位置一致,所以没有感觉,但是当加了动画以后,就会不一样啦~
感觉这个设计还是蛮好的~ 虽然不知道是不是加动画以后,presentationLayer
会持有当前的actions
~
今日份小白笔记到此结束~ 进入买买买很开心~ 写完一篇拖了很久的动画也很开心~ 下周希望不要被虐~
References:
CALayer隐式动画 https://www.jianshu.com/p/9d492373c80f
隐式动画详解 https://www.jianshu.com/p/925c4e307d86
layer的actions:https://www.jianshu.com/p/3cb404785419
强推系列:https://blog.csdn.net/u013282174/category_6014571.html