美文网首页人生几何?
[iOS] 从隐式动画开始看动画

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

作者: 木小易Ying | 来源:发表于2021-09-05 17:26 被阅读0次

    其实之前有写过《高级动画》的阅读笔记,主要是关于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,那么它具有以下两个特点:

    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 时,他会按照下面的顺序找:

    • delegate 的 -actionForLayer:forKey:
    • actions 字典
    • style 的 actions 字典
    • +defaultActionForKey:
    截屏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 有很多种方法,比如:
    • 给 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: 打个断点:

    截屏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

    相关文章

      网友评论

        本文标题:[iOS] 从隐式动画开始看动画

        本文链接:https://www.haomeiwen.com/subject/uhxliltx.html