iOS-CALayer (四)

上一篇 : iOS-CALayer (三)

前言:继续深入学习动画,主要从隐式动画、显式动画上车。

一、隐式动画

改变 CALayer 的一个可做动画的属性,并不会立刻在屏幕上体现出来。默认行为是从先前的值平滑过渡到新的值。隐式动画是未指定任何动画而发生的动画类型。改变属性值 Core Animation 根据当前事务的设置决定动画执行时间和持续时间,动画类型取决于图层行为。

1.1 事务

事务实际上是 Core Animation 包含一系列属性动画集合的机制,任何用指定的事务去改变可以做动画的图层属性都不会立刻发生变化,而是在事务提交的时候开始用一个动画过渡到新值。
事务是通过 CATransaction 类管理,CATransaction没有属性或实例方法,也不能用 alloc 和 init 创建,但可以用 begin 和 commit 分别来入栈或出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务:

  • setAnimationDuration 设置当前事务的动画时间
  • animationDuration 获取执行时间值(默认0.25s)

Core Animation 在每个 run loop 周期中自动开启新事务,及时不调用 [CATransaction begin],在任何一次 RunLoop 中都会被集中起来,做0.25s 的动画。

CATransaction 控制动画时间

//begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];

UIView 中也有方法做动画

  • +beginAnimations:context:
  • +commitAnimations
    实际上上述方法修改动画属性由于设置了CATransaction的原因。

UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。
CATransaction 的 +begin 和 +commit 方法在 +animateWithDuration:animations: 内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对 +begin 和 +commit 匹配的失误造成的风险。

1.2 完成块

UIView的block的动画方法允许在动画结束时候提供完成的动作。CATransaction 提供了 setCompletionBlock 的方法。
颜色渐变完成旋转

  //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //add the spin animation on completion
    [CATransaction setCompletionBlock:^{
        //rotate the layer 90 degrees
        CGAffineTransform transform = self.colorLayer.affineTransform;
        transform = CGAffineTransformRotate(transform, M_PI_2);
        self.colorLayer.affineTransform = transform;
    }];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
1.3 图层行为

Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但UIView把它关联的图层的这个特性关闭了。改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。

  • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的 -actionForLayer:forKey 方法。如果有,直接调用并返回结果。
  • 如果没有委托,或者委托没有实现 -actionForLayer:forKey 方法,图层接着检查包含属性名称对应行为映射的actions字典。
  • 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
  • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。

一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了 actionForLayer:forKey 的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。

    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations];

输出结果

Outside: <null>
Inside: <CABasicAnimation: 0x757f090>

当属性在动画快之外发生改变,UIView 直接返回 nil 禁止隐式动画;在动画块范围内,根据动画具体类型返回相应属性,也就是 CAbasicAnimation。
直接返回 nil 不是禁止隐式动画唯一方法, CATransacition 有方法 setDisableActions 对所有属性打开/关闭 隐式动画。[CATransacition setDisableActions:YES];

总结

1.UIView 关联图层禁用隐式动画,对这种图层做动画方法:

  • 使用 UIView 的动画函数,而不是依赖CATransacition
  • 继承 UIView 覆盖 actionForLayer:forKey
  • 创建显式动画

2.单独存在的图层,控制隐式动画方法:

  • 实现图层的 actionForLayer:forKey 方法
  • 提供 actions 字典控制隐式动画

对颜色渐变的例子使用一个不同的行为,通过给colorLayer设置一个自定义的actions字典。也可以使用委托来实现,但是actions字典可以写更少的代码。行为通常是一个被Core Animation隐式调用的显式动画对象。使用的是一个实现了CATransaction的实例,叫做推进过渡。
CATransition响应CAAction协议,并且可以当做一个图层行为就足够了。不论在什么时候改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。

- (void)viewDidLoad
{
    [super viewDidLoad];

    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add a custom action
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromLeft;
    self.colorLayer.actions = @{@"backgroundColor": transition};
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
}

- (IBAction)changeColor
{
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}

效果图
1.4呈现与模型

设置 CALayer 的属性,实际上是定义当前事务结束后图层如何显示的模型。Core Animation 负责根据图层行为和事务设置不断更新视图在屏幕上的状态。
每个图层的显示值都被存储在一个叫做呈现图层的独立图层中,可以通过 persentationLayer 方法访问。呈现图层实际上是模型图层的复制,但它的属性值代表了任何指定时刻当前外观效果。直白说,通过呈现图层的值来获取当前屏幕上真正显示出来的值。
呈现树通过图层树中所有图层的呈现图层所形成。呈现图层仅在图层首次被提交时候创建,也就是首次在屏幕上显示的时候,如果在之前调用 persentationLayer 会返回 nil 。
在呈现图层上调用 modelLayer 的方法会返回呈现所依赖的 CALayer。一般会返回 self。

移动图层的数据模型呈现

一般,不需要访问呈现图层,通过模型图层的交互让 Core Animation 更新显示。
当在同步动画处理用户交互时候,呈现图层用处很大。

  • 实现基于定时器的动画,而不是仅仅基于事务的动画,可以通过呈现图层准确知道某一时刻图层在什么位置。
  • 做动画图层响应用户输入,使用 -hitTest 方法判断指定图层是否被触摸,对呈现图层调用 - hitTest 更有意义,呈现图层代表用户当前看到的图层位置,而不是动画结束之后的位置。
- (void)layerTest{
    _imgLayer = [CALayer layer];
    _imgLayer.frame = CGRectMake(50, 50, 100, 100);
    _imgLayer.backgroundColor = [UIColor orangeColor].CGColor;
    _imgLayer.position = CGPointMake(_imgLayer.bounds.size.width*0.5+25, _imgLayer.bounds.size.width*0.5+25);
    
    [self.view.layer addSublayer:_imgLayer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self.view];
    
    if ([_imgLayer.presentationLayer hitTest:point]) {
        CGFloat red = arc4random() / (CGFloat)INT_MAX;
        CGFloat green = arc4random() / (CGFloat)INT_MAX;
        CGFloat blue = arc4random() / (CGFloat)INT_MAX;
        _imgLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    }else{
        [CATransaction begin];
        [CATransaction setAnimationDuration:2.0];
        _imgLayer.position = point;
        [CATransaction commit];
    }
}
效果图.gif

上述代码达到效果:

  • 点击图层本身变换颜色
  • 点击屏幕图层移动到相应位置

二、显式动画

2.1 基本动画

CAAnimationDelegate 在 CAAnimation 头文件或者 Apple 开发者文档中找到相关函数。
可以在 - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag 做修改相关操作。

动画完成之后修改背景颜色

- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
    //set the backgroundColor property to match animation toValue
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
    [CATransaction commit];
}

CAAnimation 使用代理模式完成相关效果会有一个问题,当一个控制器要操作多个动画,同一个回调方法,需要判断到底是哪个图层调用的。
动画本身会作为一个参数传入委托的方法,委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。
当使用 -addAnimation:forKey: 把动画添加到图层,键是 -animationForKey: 方法找到对应动画的唯一标识符,而当前动画的所有键都可以用animationKeys获取。对每个动画都关联一个唯一的键,对每个图层循环所有键,调用-animationForKey:来比对结果。
CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:和-valueForKey:方法来存取属性。但CAAnimation有一个不同的性能:它像一个NSDictionary,可以随意设置键值对,即使和使用的动画类所声明的属性并不匹配。你可以对动画用任意类型打标签。可以简单地判断动画到底属于哪个视图。

        //create transform animation
        CABasicAnimation *animation = [CABasicAnimation animation];
        [self updateHandsAnimated:NO];
        animation.keyPath = @"transform";
        animation.toValue = [NSValue valueWithCATransform3D:transform];
        animation.duration = 0.5;
        animation.delegate = self;
        [animation setValue:handView forKey:@"handView"];
        [handView.layer addAnimation:animation forKey:nil];
2.2 关键帧动画

CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,作用于单一的属性,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
关键帧起源于传动动画,是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)。你提供了显著的帧,然后Core Animation在每帧之间进行插入。

数组值改变动画

    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
                         (__bridge id)[UIColor blueColor].CGColor,
                         (__bridge id)[UIColor redColor].CGColor,
                         (__bridge id)[UIColor greenColor].CGColor,
                         (__bridge id)[UIColor blueColor].CGColor ];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];

path 路径改变动画
沿着一个贝塞尔曲线对图层做动画

- (void)createCAKeyframeAnimation{
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.view.layer addSublayer:pathLayer];
    
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 28, 28);
    shipLayer.position = CGPointMake(0, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"gbq"].CGImage;
    [self.view.layer addSublayer:shipLayer];
    
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    [shipLayer addAnimation:animation forKey:nil];
}
效果图.gif

但是发现图片方向不是跟随曲线而转变方向,增加属性:

animation.rotationMode = kCAAnimationRotateAuto;
效果图.gif
2.3 虚拟属性

比如旋转图层,可以对 transform.rotation 关键路径应用动画,而不是transform本身。

    CAKeyframeAnimation *anima = [CAKeyframeAnimation animation];
    anima.keyPath = @"transform.rotation";
    anima.repeatCount = MAXFLOAT;
    anima.duration = 0.5;   
    anima.byValue = @(M_PI * 2);
    [self.clockView.layer addAnimation:anima forKey:nil];

对 transform.rotation 做动画,而不是transform的优点:

  • 不用通过关键帧一步旋转多于180°的动画
  • 设置相对值而不是绝对值 (byValue 而不是 toValue)
  • 可以不用创建 CATransform3D,使用简单数值来指定角度。
  • 不会与 transform.position 和 transform.scale 冲突

transform.rotation 属性其实并不存在,因为CATransform3D 不是对象,而是结构体。也没有符合 KVC 相关属性。transform.rotation 实际是 CALayer 用户处理动画变换的虚拟属性。不能被直接使用,当做动画时, Core Animation 会自动通过 CAValueFunction 计算来更新transform的值。
CAValueFuncation 用于赋值给transform.rotation 简单浮点值转换用于摆放图层的CATransform3D 的矩阵值。可以通过 CAPropertyAnimation 的 valueFunction属性来改变。后续的设置覆盖默认的。
CAValueFunction看起来似乎是对那些不能简单相加的属性(例如变换矩阵)做动画的非常有用的机制,由于CAValueFunction的实现细节是私有的,不能通过继承它来自定义。可通过使用苹果目前已经提供的常量(目前都是和变换矩阵的虚拟属性相关)。

2.4 动画组

CABasicAnimation和CAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。

//create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.view.layer addSublayer:pathLayer];
    //add a colored layer
    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(0, 0, 64, 64);
    colorLayer.position = CGPointMake(0, 150);
    colorLayer.backgroundColor = [UIColor greenColor].CGColor;
    [self.view.layer addSublayer:colorLayer];
    //create the position animation
    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
    animation1.keyPath = @"position";
    animation1.path = bezierPath.CGPath;
    animation1.rotationMode = kCAAnimationRotateAuto;
    //create the color animation
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"backgroundColor";
    animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
    //create group animation
    CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[animation1, animation2];
    groupAnimation.duration = 4.0;
    //add the animation to the color layer
    [colorLayer addAnimation:groupAnimation forKey:nil];
效果图1.gif

效果图2.gif
2.5 转场动画

过渡并不想属性动画平滑的在两个值之间做动画,而是影响整个图层的变化,过渡动画首先展示之前的图层外观,然后通过交换过渡到新的外观。和基本动画不同,转场动画对图层只能使用一次。
CATransition 是 CAAnimation 的子类,有type 和 subtype 来标识变换效果。
Type 是 NSString 类型:

  • kCATransitionFade 默认效果 淡入淡出效果
  • kCATransitionMoveIn 定向滑动动画,从顶部滑动而入
  • kCATransitionPush 侧滑效果,新视图将旧视图推出去
  • kCATransitionReveal 定向滑动动画,把原始图层滑出去显示新图层

subType 控制过渡方向:

  • kCATransitionFromRight
  • kCATransitionFromLeft
  • kCATransitionFromTop
  • kCATransitionFromBottom

隐式转场动画
当设置 CALayer 的 content 属性时,CATransition 是默认行为。对于关联的图层或其他隐式动画行为,该特性依然被禁用,对于自创建的图层,对图层的 content 图片做改动都会自动附上淡入淡出动画。

自定义动画
UIView 提供过渡方法:

+transitionFromView:toView:duration:options:completion:
+transitionWithView:duration:options:animations:

options 参数如下:

  • UIViewAnimationOptionTransitionFlipFromLeft
  • UIViewAnimationOptionTransitionFlipFromRight
  • UIViewAnimationOptionTransitionCurlUp
  • UIViewAnimationOptionTransitionCurlDown
  • UIViewAnimationOptionTransitionCrossDissolve 与CATransition有关系
  • UIViewAnimationOptionTransitionFlipFromTop
  • UIViewAnimationOptionTransitionFlipFromBottom

可以通过CATransition 的 filter 属性,使用 CIFilter 创建过渡效果。
CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。
用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的时在CAAnimationDelegate中把coverView从屏幕中移除。
警告:-renderInContext:捕获了图层的图片和子图层,但不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。

动画过程取消动画
用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,但不支持在动画运行过程中修改动画,该方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
终止指定动画,从图层中移除:
- (void)removeAnimationForKey:(NSString *)key;
移除所有动画
- (void)removeAllAnimations;
动画被移除,图层的外观就立刻更新到当前的模型图层的值。动画在结束之后被自动移除,除非设置removedOnCompletion为NO,如果你设置动画在结束之后不被自动移除,当不需要的时候要手动移除它;否则它会一直存在于内存中,直到图层被销毁。

开始/停止控制旋转动画

- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.view.layer addSublayer:self.shipLayer];
}

- (void)start
{
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
}

- (void)stop
{
    [self.shipLayer removeAnimationForKey:@"rotateAnimation"];
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    //log that the animation stopped
    NSLog(@"The animation stopped (finished: %@)", flag? @"YES": @"NO");
}
下一篇 : iOS-CALayer (五)

推荐阅读更多精彩内容