[译] Core Animation 编程指南 - 高级动画技巧

1,744

本文首发地址

有很多方法去配置基于属性或者关键帧的动画,来为你做更多的事情。Apps 需要同时或顺序执行多个动画可以使用高级行为去同步多个动画的时间或将它们串在一起。你可以使用动画对象的其他类型去创建视觉形变和其它有意思的动画效果。

过渡动画支持更改图层可见性

如上述标题所示,过渡动画对象会为图层创建视觉过渡动画效果。过渡对象最常见的用法就是动画的显示一个图层,隐藏另一个图层。不像基于属性的动画,动画图层的某个属性,过渡动画操作图层的缓存图像去创建视觉效果,这个仅通过改变属性是很难或者不可能实现的。过渡的标准类型允许你执行显示、推动、移动或交叉渐变动画。在 OS X上,你也可以使用 Core Image 过滤器去创建过渡动画,可实现如擦拭、页面卷曲、波纹或自定义效果。

执行过渡动画,你需要创建一个 CATransition 对象,并将它添加到涉及过渡动画的图层上。你使用过渡对象去指定需要执行的过渡类型,即动画的起点和终点。你也不需要整个过渡动画。在动画期间,过渡对象允许你指定开始和结束的过程值。这些值允许你在动画的中点开始或者结束动画。

例 5-1 展示了实现两个视图之间创建一个推动过渡动画的代码。在该例中,myView1myView2 在同一父视图的同一个位置,但仅 myView1 可见。推动过渡动画会导致 myView1 向左边滑动消失,同时 myView2 从右向左滑动显示。更新两个视图的 hidden 属性确保在动画结束时两个视图的可见性是正确的。

例 5-1 在 iOS 上给两个视图添加过渡动画

CATransition* transition = [CATransition animation];
transition.startProgress = 0;
transition.endProgress = 1.0;
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromRight;
transition.duration = 1.0;
 
// 给两个图层添加过渡动画
[myView1.layer addAnimation:transition forKey:@"transition"];
[myView2.layer addAnimation:transition forKey:@"transition"];
 
// 最后,改变图层的可见性
myView1.hidden = YES;
myView2.hidden = NO;

// Swift 
let transition = CATransition()
transition.startProgress = 0.0
transition.endProgress = 1.0
transition.type = .push
transition.subtype = .fromRight
transition.duration = 3.0

myView1.layer.add(transition, forKey: "transition")
myView2.layer.add(transition, forKey: "transition")

myView1.isHidden = true
myView2.isHidden = false

下方为译者补全的 例 5-1 测试代码:

class ViewController: UIViewController {
    var myView1: UIView!
    var myView2: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSubviews()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let transition = CATransition()
        transition.startProgress = 0.0
        transition.endProgress = 1.0
        transition.type = .push
        transition.subtype = .fromRight
        transition.duration = 3.0

        myView1.layer.add(transition, forKey: "transition")
        myView2.layer.add(transition, forKey: "transition")

        myView1.isHidden = true
        myView2.isHidden = false
    }
}

extension ViewController {
    func setupSubviews() {
        myView2 = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
        myView2.backgroundColor = UIColor.blue
        view.addSubview(myView2)
        
        myView1 = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
        myView1.backgroundColor = UIColor.red
        view.addSubview(myView1)
    }
}

当一个过渡包含两个图层时,你可以对两个图层使用同一个过渡对象。使用同一个过渡对象可以简化你的代码。但是,如果每个图层的过渡参数是不同的,你就必须对它们使用不同的过渡对象了。

例 5-2 展示了如何使用 Core Image 过滤器在 OS X 上实现过渡效果。在用你需要的参数配置完过滤器后,将它赋值给过渡对象的 filter 属性。在这之后,应用动画的过程就和其它类型动画对象一致了。

例 5-2 使用 Core Image 过滤器在 OS X 上实现过渡动画

// Create the Core Image filter, setting several key parameters.
CIFilter* aFilter = [CIFilter filterWithName:@"CIBarsSwipeTransition"];
[aFilter setValue:[NSNumber numberWithFloat:3.14] forKey:@"inputAngle"];
[aFilter setValue:[NSNumber numberWithFloat:30.0] forKey:@"inputWidth"];
[aFilter setValue:[NSNumber numberWithFloat:10.0] forKey:@"inputBarOffset"];
 
// Create the transition object
CATransition* transition = [CATransition animation];
transition.startProgress = 0;
transition.endProgress = 1.0;
transition.filter = aFilter;
transition.duration = 1.0;
 
[self.imageView2 setHidden:NO];
[self.imageView.layer addAnimation:transition forKey:@"transition"];
[self.imageView2.layer addAnimation:transition forKey:@"transition"];
[self.imageView setHidden:YES];

注意:当在动画中使用 Core Image 过滤器时,最棘手的部分就是配置过滤器。例如,使用条形滑动过渡,如果输入角度过高或者过低都可能会导致过渡效果没有发生一样。如果你没有看见你期待的动画效果,可以调整过滤器的参数来看一下结果是否会发生变化。

自定义动画的时间

时间是动画重要的一部分,使用 Core Animation ,你可以通过 CAMediaTiming 协议的方法和属性给你的动画指定的精确时间信息。两个 Core Animation 类遵守该协议。CAAnimation 类遵守它,所以你可以给你的动画对象指定时间信息。CALayer 遵守它,所以你可以给你的隐式动画配置一些事件相关的特性,即使隐式过渡对象包含的动画通常提供优先的默认时间信息。

当讨论时间和动画时,理解图层对象是如何与时间协作是非常重要的。每个图层有它们自己的本地时间,来管理动画时间。通常,两个不同图层的本地时间足够接近,你可以给两个图层指定相同的时间值,用户通常不会察觉。但是,图层的本地时间可以被它的父图层修改,或者它自己的时间参数。例如,改变图层的 speed 属性会导致图层(包括它的子图层)上的动画持续时间按比例变化。

为协助你确保给定图层的时间值是合适的,CALayer 类定义了 convertTime:fromLayer:convertTime:toLayer: 方法。你可以使用这些方法将固定时间转为图层的本地时间,或将时间值从一个图层转到另一个图层。这些方法考虑了可能影响层的本地时间的媒体定时属性,并返回一个可以与另一层一起使用的值。在例 5-3 中,展示了如何使用常规方法获取图层的本地时间。CACurrentMediaTime() 是一个返回计算机当前时钟时间的便利函数,该方法获取时钟时间并将它转为图层的本地时间。

例 5-3 获得图层的当前本地时间

CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime() fromLayer:nil];

一旦你获得了图层的本地时间值,你可以使用该值去更新动画对象或图层的时间相关属性。通过这些时间属性,你可以实现一些有意思的动画行为,包含:

  • 使用 beginTime 属性设置动画的开始时间。通常,动画在下一个更新循环期间开始。你可以使用 beginTime 参数将动画的开始时间延迟几秒钟。将两个动画串在一起的方式是:将一个动画的开始时间设置为另一个动画的结束时间。如果你延迟了动画的开始,你可能还想把 fillMode 属性值设置为 kCAFillModeBackwards 。该填充模式会造成图层显示动画的开始值,即使图层树中的图层对象包含不同的值。不用该填充模式,你将会看到在动画执行之前调到结束值。其他的模式也是有效的。
  • autoreverses 属性会使动画在指定期间内执行,然后返回到动画初始值状态。你可以将该属性和 repeatCount 属性联合使用,在开始值和结束值之间来回动画显示。将自动复制动画的重复计数设置为整数(如1.0),会导致动画在其起始值停止。添加额外的半步(例如重复计数为1.5)会导致动画在其结束值停止。
  • 组动画使用 timeOffset 属性可以在比其它动画更晚的时间开始一些动画。

暂停、恢复动画

为了暂停动画,你可以利用图层遵守 CAMediaTiming 协议的事实,将图层动画的速率设置为 0.0。将速率改为非零值即恢复动画。例 5-4 展示了如何暂停恢复图层动画的代码实现。

例 5-4 暂停重启图层的动画

- (void)pauseLayer:(CALayer*)layer {
   CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
   layer.speed = 0.0;
   layer.timeOffset = pausedTime;
}
 
- (void)resumeLayer:(CALayer*)layer {
   CFTimeInterval pausedTime = [layer timeOffset];
   layer.speed = 1.0;
   layer.timeOffset = 0.0;
   layer.beginTime = 0.0;
   CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
   layer.beginTime = timeSincePause;
}

显式事务允许你改变动画参数

你做的每个图层的改变必须是事务的一部分。CATransaction 类管理动画的创建和组织,让它们在合适的时间执行。在多数情况下,你不需要创建自己的事务。无论何时你给图层添加隐式或显示动画,Core Animation 都会自动创建隐式事务。然而,你也可以创建显式事务来更精确的管理动画。

你使用 CATransaction 类的方法去创建管理事务。调用 begin 类方法去开始新的事务;调用 commit 类方法去终止事务。在这两个方法之间是你想要的事务改变的部分。例如,改变图层的两个属性,你可以使用例 5-5 的代码。

例 5-5 创建显式事务

[CATransaction begin];
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
[CATransaction commit];

使用事务的一个主要原因是,在显式事务范围内,你可以修改持续时间,时间函数,和其它的参数。你也可以赋值一个 completion block 去获取事务,当组动画完成的时候,使你的 app 可以被通知。使用 setValue:forKey: 方法和事务字典包含的键来改变响应的动画参数。例如,将默认的持续时间改为 10 秒,你将改变 kCATransactionAnimationDuration 键的值,如例 5-6 所示。

例 5-6 改变动画默认的持续时间

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:10.0f]
                 forKey:kCATransactionAnimationDuration];
// Perform the animations
[CATransaction commit];

在你想给集合中不同动画设置不同值得时候,你可以嵌入事务。将事务嵌入到另一个事务,只需再调一次 begin 类方法即可。每一次 begin 方法的调用都要有相匹配的 commit 方法的调用。只有在提交最外层事务的更改后,Core Animation 才会开始相关的动画。

例 5-7 展示了将一个事务嵌入到另一个事务的例子。在该例中,内部事务更改与外部事务相同的动画参数,但使用不同的值。

例 5-7 嵌套显式事务

[CATransaction begin]; // Outer transaction
 
// Change the animation duration to two seconds
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
                forKey:kCATransactionAnimationDuration];
// Move the layer to a new position
theLayer.position = CGPointMake(0.0,0.0);
 
[CATransaction begin]; // Inner transaction
// Change the animation duration to five seconds
[CATransaction setValue:[NSNumber numberWithFloat:5.0f]
                 forKey:kCATransactionAnimationDuration];
 
// Change the zPosition and opacity
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
 
[CATransaction commit]; // Inner transaction
 
[CATransaction commit]; // Outer transaction

给动画添加透视效果

Apps 可以在三维空间中操作图层,但是为了简单起见,Core Animation 使用平行投影显示图层,这基本上将场景展平为二维平面。这种默认行为会导致具有不同 zPosition 值的大小相同的图层显示为相同的大小,即使它们在z轴上相距很远。你通常会看到三维场景的点已经消失了。但是,你可以通过修改图层的转换矩阵以包含透视信息来更改该行为。

修改场景透视图时,需要修改包含正在查看的图层的最高层的 sublayerTransform 矩阵。通过将相同的透视信息应用于所有子层,修改最高级简化了你必须写的代码。它还确保透视正确应用于在不同平面中彼此重叠的同级子图层。

例 5-8 展示了为父图层创建简单透视变换的方法。在这种情况下,eyePosition 变量指定沿 z 轴查看图层的相对距离。通常,你为 eyePosition 指定一个正值,以保持图层以预期的方式定向。值越大,场景越平坦,而值越小,图层间的视觉差异越大。

例 5-8 给父图层添加透视形变

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0/eyePosition;
 
// Apply the transform to a parent layer.
myParentLayer.sublayerTransform = perspective;

通过父图层的配置,你可以改变任何子图层的 zPosition 属性,观察它们的大小如何基于它们与眼的相对距离而变化的。

最后