读 Animations Explained 笔记

原文地址:http://www.objc.io/issue-12/animations-explained.html

1. 当给layer添加animations时,是不会直接修改其属性值的。

2. Core Animation维护两个平行的layer层次结构:分别时:model layer tree(模态层树)和presentation layer tree(表示层树)。注:实际上还有第三层树,称为:rendering tree(渲染树),它对Core Animation来说是私有的 。

3. 通过 -[CALayer presentationLayer] 和 -[CALayer modelLayer] 可以在模态层树和表示层树之间进行切换。


4. 例子一:基本动画——利用CABasicAnimation实现一个从上往下运动的动画,如下代码:

    CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"position.y"];
    basicAnimation.fromValue = @(_aView.layer.position.y);
    basicAnimation.toValue = @640;
    basicAnimation.duration = 1;
    
    [_aView.layer addAnimation:basicAnimation forKey:@"basic"];

第一点提到,当给layer添加动画时,是不会直接修改其属性的(动画运动时改变的只是表示层属性的值,而当运动结束后,这个animation对象会被移除,表示层的值又会恢复到模态层的值),所以当动画执行完后,视图又会跳动回原来的位置。解决这个问题的方法有两个:方法一是 直接在模态层上更新属性的值。这是推荐的方法,因为它使得动画完全可选。如下:

_aView.layer.position = CGPointMake(_aView.layer.position.y, 640);
方法二是 通过将layerd的 fillMode属性设置为 kCAFillModeForword 告诉动画保留最后的状态,并且通过将 removedOnCompletion设置为NO 来防止动画被自动移除。在添加动画之前添加如下代码:

basicAnimation.fillMode = kCAFillModeForwards;
    basicAnimation.removedOnCompletion = NO;

注:将已经完成了的动画保持在layer上会造成额外的开销,因为渲染器会去进行额外的绘画工作。

        上面的动画看起来会很不自然,因为现实时间中大部分运动需要时间来加速或减速。这个问题可以通过引入一个时间函数(timing function)(有时也被称为 easing 函数)来解决。该函数通过修改持续时间的分数来控制动画的速度:

a. 最简单的 easing 函数是 linear。它在动画上维持一个不变的速度。在 Core Animation 上,这个函数是通过 CAMediaTimingFunction 类来表示的。如下代码演示:

CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;

animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

[rectangle.layer addAnimation:animation forKey:@"basic"];

rectangle.layer.position = CGPointMake(150, 0);

读 Animations Explained 笔记_第1张图片

Core Animation 还提供了其他的一些easing 函数,如:

  • Ease in (kCAMediaTimingFunctionEaseIn): 
    读 Animations Explained 笔记_第2张图片
  • Ease out (kCAMediaTimingFunctionEaseOut): 
    读 Animations Explained 笔记_第3张图片
  • Ease in ease out (kCAMediaTimingFunctionEaseInEaseOut): 
    读 Animations Explained 笔记_第4张图片
  • 默认 (kCAMediaTimingFunctionDefault): 
    读 Animations Explained 笔记_第5张图片
在限定内,也可以使用 +functionWithControlPoints:::: 来创建自己的 easing 函数。通过传递一个 cubic 贝塞尔曲线的两个控制点 x 和 y,你可以简单的自定义 easing 函数。比如为例子一选择的那个:

CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"position.y"];
    basicAnimation.fromValue = @(_aView.layer.position.y);
    basicAnimation.toValue = @640;
    basicAnimation.duration = 1;
    
    basicAnimation.fillMode = kCAFillModeForwards;
    basicAnimation.removedOnCompletion = NO;
    
    basicAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.5 :0 :0.9 :0.7];
   
    [_aView.layer addAnimation:basicAnimation forKey:@"basic"];
传递给 +functionWithControlPoints:::: 的值有效地控制了控制点的位置。所得到的定时函数将基于得到的路径来调整动画的速度。x 轴代表时间的分数,而 y 轴是插值函数的输入值。
读 Animations Explained 笔记_第6张图片
但是,由于这些部分被锁定在 [0–1] 的范围内,我们不可能用它来创建一些像预期动作 (Anticipation,一种像目标进发前先回退一点,到达目标后还过冲一会儿,见下图) 这样的常见效果。

作者写了一个小型库,叫 RBBAnimation ,它包含一个允许使用 更多复杂 easing 函数 的自定义子类 CAKeyframeAnimation,包括反弹和包含负分量的 cubic Bézier 函数:

读 Animations Explained 笔记_第7张图片

RBBTweenAnimation *animation = [RBBTweenAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;

animation.easing = RBBCubicBezier(0.68, -0.55, 0.735, 1.55);

读 Animations Explained 笔记_第8张图片

RBBTweenAnimation *animation = [RBBTweenAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;

animation.easing = RBBEasingFunctionEaseOutBounce;

5. 在创建的动画对象被添加到 layer 时,是会被复制一份的。可以通过这个特性来多个view中重用动画。如下是让第二个view在第一个view移动 0.5秒后跟着也移动:

CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"position.y"];
    basicAnimation.fromValue = @(_aView.layer.position.y);
    basicAnimation.toValue = @640;
    basicAnimation.duration = 1;

    [_aView.layer addAnimation:basicAnimation forKey:@"basic"];
    _aView.layer.position = CGPointMake(_aView.layer.position.x, 640);
    
    basicAnimation.beginTime = CACurrentMediaTime() + 0.5;
    
    [_bView.layer addAnimation:basicAnimation forKey:@"basic"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [_bView setValue:@640 forKeyPath:@"layer.position.y"];
    });
原文中使用的是 byValue 来实现的。但是在运行代码的时候发现通过byValue来设置时动画会出现延迟,即_aView.layer.position = CGPointMake(_aView.layer.position.x, 640); 会先产生效果,从而使执行动画的view先跳到指定的位置,再执行动画。当然,也可以让 _aView.layer.position = CGPointMake(_aView.layer.position.x, 640);  和 _bView.layer.position = CGPointMake(_bView.layer.position.x, 640); 延时调用(让动画执行完后再调用),从而达到效果。

6. 例子二:多步动画——利用CAKeyframeAnimation可以定义多个中断的点,然后让Core Animation来填充中间帧,从而实现多步动画。下面的代码是让一个密码输入框和被点击的按钮进行抖动,如下:

- (IBAction)touchKeyframe:(id)sender {
    [self keyframeAnimationWithView:_passwordField];
    [self keyframeAnimationWithView:sender];
}

- (void)keyframeAnimationWithView:(UIView *)view {
    CAKeyframeAnimation *keyframe = [CAKeyframeAnimation animation];
    keyframe.keyPath = @"position.x";
    
    keyframe.values = @[@0, @10, @(-10), @10, @0];
    keyframe.keyTimes = @[@0, @(1/6.0), @(3/6.0), @(5/6.0), @1];
    keyframe.duration = 0.4;
    
    keyframe.additive = YES;
    
    [view.layer addAnimation:keyframe forKey:@"shake"];
}
values 定义了view应该运动到哪些位置;

keyTimes 指定了哪一个时间点触发关键帧;

additive 设置为YES 是告诉Core Animation 将由动画指定的值添加为当前的渲染树的值。这使我们能够不需要知道执行动画的view的初始位置 为需要更新位置的view重复使用相同的动画(比如:执行动画的view原始位置为(x0, y0),如果不通过将additive属性设置为YES的话,那values就应该是 @[@(0+x0), @(10+x0), @(-10+x0), ...])了)。这个属性也可以用在CABasicAnimation上。


7. 例子三:沿路径的动画——通过CAKeyframeAnimation的path属性可以很简单的实现路径动画。下面的代码是让一个view做一个沿圆形路径旋转的动画:

CGRect boundingRect = CGRectMake(-150, -150, 150, 150);
    CAKeyframeAnimation *keyframe = [CAKeyframeAnimation animation];
    keyframe.keyPath = @"position";
    
    keyframe.path = CFAutorelease(CGPathCreateWithEllipseInRect(boundingRect, NULL));

    keyframe.duration = 4;
    keyframe.additive = YES;
    keyframe.repeatCount = HUGE_VALF;
    keyframe.calculationMode = kCAAnimationPaced;
    keyframe.rotationMode = kCAAnimationRotateAuto;
    
    [_pathView.layer addAnimation:keyframe forKey:@"orbit"];
使用 CGPathCreateWithEllipseInRect() 创建一个圆形的CGPath来作为keyframeAnimation的path。

使用 calculationMode 是另一种控制关键帧动画时间的方法。通过将其设置为 kCAAnimationPaced,让Core Animation 为动画使用一个不变的速度,不管path的各个路径有多长。 calculationMode 有以下几个值可以设置:

CA_EXTERN NSString * const kCAAnimationLinear;
CA_EXTERN NSString * const kCAAnimationDiscrete;
CA_EXTERN NSString * const kCAAnimationPaced;
CA_EXTERN NSString * const kCAAnimationCubic;
CA_EXTERN NSString * const kCAAnimationCubicPaced;

设置 rotationMode 属性为 kCAAnimationRotateAuto 以确保执行动画的view 沿着路径旋转。可以将这个属性设置为nil对比下效果。 rotationMode 有以下两个值可以设置:

CA_EXTERN NSString * const kCAAnimationRotateAuto;
CA_EXTERN NSString * const kCAAnimationRotateAutoReverse;


8. 动画组——就是把几个单一的动画组合起来在同一个view上执行。可以通过 CAAnimationGroup来实现。如下代码:

CABasicAnimation *zPosition = [CABasicAnimation animation];
    zPosition.keyPath = @"zPosition";
    zPosition.fromValue = @-1;
    zPosition.toValue = @1;
    zPosition.duration = 1.2;
    
    CAKeyframeAnimation *rotation = [CAKeyframeAnimation animation];
    rotation.keyPath = @"transform.rotation";
    rotation.values = @[ @0, @0.14, @0 ];
    rotation.duration = 1.2;
    rotation.timingFunctions = @[
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
                                 ];
    
    CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
    position.keyPath = @"position";
    position.values = @[
                        [NSValue valueWithCGPoint:CGPointZero],
                        [NSValue valueWithCGPoint:CGPointMake(110, -20)],
                        [NSValue valueWithCGPoint:CGPointZero]
                        ];
    position.timingFunctions = @[
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
                                 ];
    position.additive = YES;
    position.duration = 1.2;
    
    CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
    group.animations = @[ zPosition, rotation, position ];
    group.duration = 1.2;
//    group.beginTime = 0.5;

    [_aView.layer addAnimation:group forKey:@"group"];
上面我注释了 group.beginTime = 0.5;  这句代码。原文中是存在这句代码的,但是我在运行的时候因为这句代码,动画不执行,不知道原因。

扩展阅读

  • Core Animation 编程指南
  • 动画的 12 个基本原则
  • 使用 CAShapeLayer 的 CGPath 动画绘图
  • 控制动画时间
  • pop
  • RBBAnimation



你可能感兴趣的:(动画,animation)