UI基础 - 动画:CAAnimation | CATransaction

前言

1 - CAAnimation 并不是一个单纯的实现动画的框架,它原本叫 Layer Kit。管理着树状结构的图层数据,并快速组合这些图层,最终构成了一切可视化的基础

2 - 在构建可视化,也就是视图的时候,iOS 中使用 UIKit 中的 UIView;mac OS 中使用 AppKit 中的 NSView;但是它们的 layer 都是 CALayer。这是因为鼠标键盘和触摸屏差距很大,为了方便跨平台、解耦等原因而进行了功能的拆分,于是就有了 CoreAnimation 和 UIKit/AppKit

CALayer 的三个树状结构

1 - model layer tree 模型树:它存储图层属性的目标值,在没有动画的时候,目标值就是当前显示的效果:super -> sub1 + sub2 +..+sub3->sub3.1+...没错,就是父层与子层的树状结构

2 - presentation tree 显示树:它存储图层属性的当前值,在动画的过程中它是不断变化的、是只读的。 CALayer 对象有一个 presentationLayer 属性,可以读取当前的动画状态,因此presentation tree 和 model layer tree 的对象是一一对应的

3 - render tree 渲染树:用于执行动画,这是图层的私有对象,Apple 没有对它进行解释

注:模型树只会保存属性的最终值;而显示树会把初始值到最终值期间的过程值全部算一遍,通过 layer 的 presentationLayer 属性访问。并且它也是一个 CALayer,在 layer 第一次显示出来的时候被创建。在属性动画的过程中,它的对应属性值一直在发生变化。例如一个 position 动画,layer 在运动中如果想要知道有没有点击到 layer,此时需要 presentationLayer 的 hitTest 来判断,而不是 layer 自己的 hitTest(可在 touchbegin 中使用 [self.layer.presentationLayer hitTest:point] 来判定)

CATransaction

1 - 隐式动画

① CoreAnimation 的隐式动画:之所以叫隐式,是因为我们只是修改了一个属性的值,并没有指定任何动画、更没有定义动画如何执行,但是系统自动产生了动画

@interface ViewController()
@property(strong,nonatomic)CALayer *layer;
@end 
_layer = [CALayer layer];
_layer.frame = CGRectMake(100, 100, 200, 200);
_layer.backgroundColor = UIColor.blackColor.CGColor;
[self.view.layer addSublayer:_layer];
// 以 frame 为例:当 CALayer 改变 frame 时是一个动画(0.25秒)
_layer.frame = CGRectMake(200, 300, 100, 80);

② setAnimationDuration 和 animationDuration 可以设置和获取当前事务的执行时长

 1 // 同时修改 position、backgroundColor,并且设置时长
 2 CGPoint po = _layer.position;
 3 [CATransaction begin];
 4     
 5 [CATransaction setAnimationDuration:3.0];
 6 _layer.backgroundColor = [UIColor cyanColor].CGColor;
 7 _layer.position = CGPointMake(po.x + arc4random_uniform(100), po.y + arc4random_uniform(100));
 8     
 9 [CATransaction commit];
10 // 动画结束
11 [CATransaction setCompletionBlock:^{
12     self->_layer.backgroundColor = UIColor.blackColor.CGColor;
13 }];

这一堆动画属性是不是非常眼熟?是的,animateWithDuration:、animations:completion: 实际就是对 CATransaction 的封装。另外 UIView 的 beginAnimations:context: 、 commitAnimations 分别对应 CATransaction 的 begin、commit

注:将 ① 中的 CALayer 换成 UIView,会发现动画没了。这是因为 UIKit 把隐式动画给禁了。即便使用 ② 中的代码,搭配 CATransaction 的 begin 和 commit,UIView 也不会产生动画。为什么?

  当 CALayer 的属性被修改时,图层会首先检测它是否有代理并且是否实现 CALayerDelegate 的 actionForLayer:forKey 方法。如果有则直接调用并返回结果

  如果没有,则图层会接着检查包含属性名称对应行为映射的 actions 字典,如果 actions 字典没有包含对应的属性,那么图层就会在它的 style 字典接着搜索属性名

  如果在 style 里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的 defaultActionForKey: 方法

    所以一轮完整的搜索结束之后,actionForKey: 要么返回空,这时没有动画;要么是 CAAction 协议对应的对象,最后 CALayer 拿这个结果去对先前和当前的值做动画

      因为每个 UIView 对它关联的 layer 都实现了 actionForLayer:forKey 方法,如果添加了事务,那么返回非空;如果没有,则返回空。因此 animateWithDuration:animations:completion: 中的代码是可以执行动画的,否则没有动画。这个方法的 block 会给到每个 CATransaction

③ 事务 CATransaction 便是 CoreAnimation 处理属性动画的机制。它没有构造方法、没有实例方法、只有一些类方法。通过 begin 和 commit 来入栈和出栈。注:在一次 runloop 中任何 Animatable 属性发生变化,都会向栈顶加入新的事务

2 - 显示动画

① 知道了 CoreAnimation 的隐式动画,那么 CAAnimation 就是显式动画了。UIView 禁止了隐式动画,但是 CAAnimation 可以为 UIView 的 layer 添加显式动画。显式动画并没有修改属性的值,只是执行动画而已,因此还需要主动修改属性。注:如果在给没有绑定 UIView 的 CALayer 对象添加 CAAnimation 时,由于这个 layer 带有隐式动画,所以一但修改属性的值,再加上 CAAnimation,这就会出现两次动画

// 初始效果
_layer = [CALayer layer];
_layer.frame = CGRectMake(100, 100, 200, 200);
_layer.backgroundColor = UIColor.blackColor.CGColor;
[self.view.layer addSublayer:_layer];
// 设置动画
CGPoint po = _layer.position;
CGPoint po1 = CGPointMake(po.x + 50, po.y + 50);

CABasicAnimation *a1 = [CABasicAnimation animationWithKeyPath:@"position"]
a1.fromValue = [NSValue valueWithCGPoint:po];
a1.toValue = [NSValue valueWithCGPoint:po1];
a1.duration = 2.0;
[_layer addAnimation:a1 forKey:nil];
_layer.position = po1;

产生问题:会出现两次动画效果,第 1 次是隐式动画(0.25秒);第 2 次是自己配置的显示动画(2.0秒)。解决方案如下

 1 CGPoint po = _layer.position;
 2 CGPoint po1 = CGPointMake(po.x + 50, po.y + 50);
 3 
 4 CABasicAnimation *a1 = [CABasicAnimation animationWithKeyPath:@"position"];
 5 a1.fromValue = [NSValue valueWithCGPoint:po];
 6 a1.toValue = [NSValue valueWithCGPoint:po1];
 7 a1.duration = 2.0;
 8 // 方案一 在动画执行之前赋值
 9 _layer.position = po1;
10     
11 // 方案二:禁用 CATransaction 的隐式动画
12 // CATransaction.disableActions = YES;
13     
14 [_layer addAnimation:a1 forKey:nil];
15     
16 
17 // 方案三:要么在动画完全结束之后赋值:需要在 CAAnimationDelegate 的 animationDidStop:finished:中实现
18 // 方案四:配合 dispatch_after 使用

② 代码示例:创建两个动画视图 _layer 和 TestView,在 touchesBegan:withEvent: 方法中熟悉动画效果

  1 #import "ViewController.h"
  2 #import "SecondViewController.h"
  3 #import <UIKit/UIKit.h>
  4 
  5 // TestView Class
  6 @interface TestView : UIView
  7 
  8 @end
  9 @implementation TestView
 10 
 11 @end
 12 
 13 // ViewController
 14 @interface ViewController()
 15 @property(strong,nonatomic)CALayer *layer;
 16 @end
 17 
 18 @implementation ViewController
 19 
 20 - (void)viewDidLoad {
 21     [super viewDidLoad];
 22     self.view.backgroundColor = [UIColor cyanColor];
 23     
 24     // 显示动画
 25     TestView *tView = [[TestView alloc] initWithFrame:CGRectMake((SCREEN_WIDTH - 200)/2.0, 380, 200, 160)];
 26     tView.backgroundColor = [UIColor redColor];
 27     [self.view addSubview:tView];
 28 
 29     // CATransaction
 30     _layer = [CALayer layer];
 31     _layer.frame = CGRectMake(100, 100, 160, 80);
 32     _layer.backgroundColor = UIColor.blackColor.CGColor;
 33     [self.view.layer addSublayer:_layer];
 34 }
 35 
 36 // CATransaction
 37 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
 38 
 39      CATransition *transition = [CATransition animation];
 40      transition.type = kCATransitionPush;
 41      transition.subtype = kCATransitionFromLeft;
 42      // 当执行事务时,也就是改变了 backgroundColor
 43      // 这时 layer 搜索了 actions 字典,发现 backgroundColor 对应有值 transition,于是就执行这个 transition 动画
 44      _layer.actions = @{@"backgroundColor": transition};
 45 
 46      [CATransaction begin];
 47      [CATransaction setAnimationDuration:3.0];
 48      _layer.backgroundColor = [UIColor yellowColor].CGColor;
 49      [CATransaction commit];
 50  }
 51 
 52 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
 53     UITouch *touch = [touches anyObject];
 54 
 55     //------------------------------------------------------ UIView 动画
 56     if ([touch.view isEqual:self.view]) {
 57 
 58         // 执行 回调 动画块
 59         [UIView beginAnimations:@"回调" context:nil];
 60 
 61         [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; // 过渡曲线
 62         [UIView setAnimationDuration:3.0];// 时长
 63         [UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:self.view cache:YES];// 翻页效果
 64         [UIView setAnimationDelegate:self];// 代理:animationDidStop:finished:
 65         // 背景颜色
 66         if ([self.view.backgroundColor isEqual:[UIColor whiteColor]]) {
 67             self.view.backgroundColor = [UIColor yellowColor];
 68         }else{
 69             self.view.backgroundColor = [UIColor whiteColor];
 70         }
 71 
 72         [UIView commitAnimations];
 73         return;
 74     }
 75 
 76     //------------------------------------------------------ CATransaction
 77     TestView *aView = (TestView *)touch.view;
 78     [CATransaction begin];
 79 
 80     // kCATransactionAnimationDuration
 81     // kCATransactionDisableActions
 82     // kCATransactionAnimationTimingFunction
 83     // kCATransactionCompletionBlock
 84     [CATransaction setValue:@5.0 forKey:kCATransactionAnimationDuration];
 85 
 86 
 87     // 动画效果一:尺寸缩放
 88     // path 输入不同的字符串,可以创建相应属性变化的动画效果
 89     // opacity:透明度       transform.scale:图层大小
 90     CABasicAnimation *shrinkAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
 91 
 92     // 枚举说明
 93     // kCAMediaTimingFunctionLinear
 94     // kCAMediaTimingFunctionEaseIn
 95     // kCAMediaTimingFunctionEaseOut
 96     // kCAMediaTimingFunctionEaseInEaseOut
 97     // kCAMediaTimingFunctionDefault
 98     // 加速减速模式
 99     shrinkAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
100     // 终点大小 = 原始大小 * toValue
101     shrinkAnimation.toValue = @0.5;// toValue 是 0 - 1 之间的数
102     [aView.layer addAnimation:shrinkAnimation forKey:@"shrinkAnimation"];
103 
104 
105     // 动画效果二:渐变透明
106     CABasicAnimation *fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
107     fadeAnimation.toValue = @0.2;
108     fadeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
109     [aView.layer addAnimation:fadeAnimation forKey:@"fadeAnimation"];
110 
111 
112     // 动画效果三:弹簧
113     CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
114     CGMutablePathRef positionPath = CGPathCreateMutable(); // 创建一个位置块
115     CGPathMoveToPoint(positionPath, NULL, aView.layer.position.x, aView.layer.position.y);// 将位置块与图层绑定
116 
117     // 第一个参数:位置快    第二个参数:映射变化矩阵,不需要时置 NULL
118     // 第三第四个参数是指动画经过的坐标     第五第六个参数是指动画结束的坐标
119     CGPathAddQuadCurveToPoint(positionPath, NULL, aView.layer.position.x, -aView.layer.position.y, aView.layer.position.x, aView.layer.position.y);
120     CGPathAddQuadCurveToPoint(positionPath, NULL, aView.layer.position.x, -aView.layer.position.y*0.7, aView.layer.position.x, aView.layer.position.y);
121     CGPathAddQuadCurveToPoint(positionPath, NULL, aView.layer.position.x, -aView.layer.position.y*0.45, aView.layer.position.x, aView.layer.position.y);
122     CGPathAddQuadCurveToPoint(positionPath, NULL, aView.layer.position.x, -aView.layer.position.y*0.25, aView.layer.position.x, aView.layer.position.y);
123     positionAnimation.path = positionPath;
124     positionAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
125     [aView.layer addAnimation:positionAnimation forKey:@"positionAnimation"];
126 
127 
128     // 提交动画
129     [CATransaction commit];
130 }
131 
132 
133 @end

 

posted on 2022-09-29 01:52  低头捡石頭  阅读(130)  评论(0编辑  收藏  举报

导航