人生几何?

[iOS] 从隐式动画开始看动画

2021-09-05  本文已影响0人  木小易Ying

其实之前有写过《高级动画》的阅读笔记,主要是关于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:

如果我不想有这个动画肿么破呢?

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,那么它具有以下两个特点:

  1. 直接对它赋值可能产生隐式动画;
  2. 我们的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

截屏2021-09-05 上午9.17.08.png

是不是很神奇,我们虽然调用的是 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,为啥呢?

截屏2021-09-05 下午3.33.22.png

看起来就是我们通过addAnimation会把创建的 CAAction 和 key 绑定起来,但是不是放进 actions 的字典哦,因为我发现,加完动画以后打印 layer 的 actions 还是nil哦:

截屏2021-09-05 下午3.48.15.png

所以 CAAnimation 和 layer 的 actions 关系不大,CAAnimation一但被添加,layer就会动起来(如果没有设置delay),因为它已经拿到了 CAAction。但是如果是你随便改了 layer 的一个属性,却没有告诉他用什么 CAAction,那么他就需要先找 layer 要 action,而要 action 的过程就是下面酱紫的。

4. CALayer 的 actionForKey 怎么找

注意下图是 CALayer 的 actionForKey 方法,不要和它的 delegate 里面的方法弄混哦~ 当向 layer 找 action 时,他会按照下面的顺序找:

截屏2021-09-05 下午3.53.19.png

这个时候我最感兴趣的是,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 有很多种方法,比如:
触发上面的方法的时机

5. CATransaction 触发 action

CATransaction是怎么做的呢?我们知道其实他和 UIView animate 类似,就是只是指明了要改啥,对怎么改能设定的余地不多,主要是duration可以,那么当我们使用 CATransaction 的时候,有木有触发 actionForKey 呢?

        [CATransaction begin];
        [CATransaction setAnimationDuration:3];
        self.view1.alpha = 0.5;
        [CATransaction commit];

然后我们给 View1 的 actionForLayer:forKey: 打个断点:

截屏2021-09-05 下午5.10.37.png

果然是触发了,毕竟它木有一个现成的 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 可能并不是只看这四个,还有其他的逻辑。因为木有查到源码只能作罢,但隐式动画好像有点特殊~

截屏2021-09-05 下午10.28.46.png

此外,不瞒你说,我并没有发现什么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,只是因为没有动画的时候 presentationLayermodelLayer 位置一致,所以没有感觉,但是当加了动画以后,就会不一样啦~

感觉这个设计还是蛮好的~ 虽然不知道是不是加动画以后,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

上一篇下一篇

猜你喜欢

热点阅读