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