原文地址: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);
- Ease in (
kCAMediaTimingFunctionEaseIn
):
- Ease out (
kCAMediaTimingFunctionEaseOut
):
- Ease in ease out (
kCAMediaTimingFunctionEaseInEaseOut
):
- 默认 (
kCAMediaTimingFunctionDefault
):
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 轴是插值函数的输入值。
但是,由于这些部分被锁定在 [0–1] 的范围内,我们不可能用它来创建一些像预期动作 (Anticipation,一种像目标进发前先回退一点,到达目标后还过冲一会儿,见下图) 这样的常见效果。
作者写了一个小型库,叫 RBBAnimation ,它包含一个允许使用 更多复杂 easing 函数 的自定义子类 CAKeyframeAnimation,包括反弹和包含负分量的 cubic Bézier 函数:
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);
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; 这句代码。原文中是存在这句代码的,但是我在运行的时候因为这句代码,动画不执行,不知道原因。
扩展阅读