9. 图层时间

图层时间

时间和空间最大的区别在于,时间不能被复用 — 弗斯特梅里克

在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。

9.1 CAMediaTiming协议

CAMediaTiming`协议

CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayerCAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

持续和重复

我们在第八章“显式动画”中简单提到过duration(CAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。

这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做repeatCount,代表动画重复的迭代次数。如果duration是2,repeatCount设为3.5(三个半迭代),那么完整的动画时长将是7秒。

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
iOS核心动画高级技巧 - 5 - 图1

durationrepeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。

清单9.1 测试durationrepeatCount

  1. @interface ViewController ()
  2. @property (nonatomic, weak) IBOutlet UIView *containerView;
  3. @property (nonatomic, weak) IBOutlet UITextField *durationField;
  4. @property (nonatomic, weak) IBOutlet UITextField *repeatField;
  5. @property (nonatomic, weak) IBOutlet UIButton *startButton;
  6. @property (nonatomic, strong) CALayer *shipLayer;
  7. @end
  8. @implementation ViewController
  9. - (void)viewDidLoad
  10. {
  11. [super viewDidLoad];
  12. //add the ship
  13. self.shipLayer = [CALayer layer];
  14. self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
  15. self.shipLayer.position = CGPointMake(150, 150);
  16. self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
  17. [self.containerView.layer addSublayer:self.shipLayer];
  18. }
  19. - (void)setControlsEnabled:(BOOL)enabled
  20. {
  21. for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
  22. control.enabled = enabled;
  23. control.alpha = enabled? 1.0f: 0.25f;
  24. }
  25. }
  26. - (IBAction)hideKeyboard
  27. {
  28. [self.durationField resignFirstResponder];
  29. [self.repeatField resignFirstResponder];
  30. }
  31. - (IBAction)start
  32. {
  33. CFTimeInterval duration = [self.durationField.text doubleValue];
  34. float repeatCount = [self.repeatField.text floatValue];
  35. //animate the ship rotation
  36. CABasicAnimation *animation = [CABasicAnimation animation];
  37. animation.keyPath = @"transform.rotation";
  38. animation.duration = duration;
  39. animation.repeatCount = repeatCount;
  40. animation.byValue = @(M_PI * 2);
  41. animation.delegate = self;
  42. [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
  43. //disable controls
  44. [self setControlsEnabled:NO];
  45. }
  46. - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
  47. {
  48. //reenable controls
  49. [self setControlsEnabled:YES];
  50. }
  51. @end

[图片上传失败…(image-a19a48-1576128672111)]

图9.2 摆动门的动画

对门进行摆动的代码见清单9.2。我们用了autoreverses来使门在打开后自动关闭,在这里我们把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCountINFINITY也有同样的效果。注意repeatCount和repeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。

清单9.2 使用autoreverses属性实现门的摇摆

  1. @interface ViewController ()
  2. @property (nonatomic, weak) UIView *containerView;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad
  6. {
  7. [super viewDidLoad];
  8. //add the door
  9. CALayer *doorLayer = [CALayer layer];
  10. doorLayer.frame = CGRectMake(0, 0, 128, 256);
  11. doorLayer.position = CGPointMake(150 - 64, 150);
  12. doorLayer.anchorPoint = CGPointMake(0, 0.5);
  13. doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
  14. [self.containerView.layer addSublayer:doorLayer];
  15. //apply perspective transform
  16. CATransform3D perspective = CATransform3DIdentity;
  17. perspective.m34 = -1.0 / 500.0;
  18. self.containerView.layer.sublayerTransform = perspective;
  19. //apply swinging animation
  20. CABasicAnimation *animation = [CABasicAnimation animation];
  21. animation.keyPath = @"transform.rotation.y";
  22. animation.toValue = @(-M_PI_2);
  23. animation.duration = 2.0;
  24. animation.repeatDuration = INFINITY;
  25. animation.autoreverses = YES;
  26. [doorLayer addAnimation:animation forKey:nil];
  27. }
  28. @end

相对时间

每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。

beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。

speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。

timeOffsetbeginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。

beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。

可以用清单9.3的测试程序验证一下,设置speedtimeOffset滑块到随意的值,然后点击播放来观察效果(见图9.3)

清单9.3 测试timeOffsetspeed属性

  1. @interface ViewController ()
  2. @property (nonatomic, weak) IBOutlet UIView *containerView;
  3. @property (nonatomic, weak) IBOutlet UILabel *speedLabel;
  4. @property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
  5. @property (nonatomic, weak) IBOutlet UISlider *speedSlider;
  6. @property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
  7. @property (nonatomic, strong) UIBezierPath *bezierPath;
  8. @property (nonatomic, strong) CALayer *shipLayer;
  9. @end
  10. @implementation ViewController
  11. - (void)viewDidLoad
  12. {
  13. [super viewDidLoad];
  14. //create a path
  15. self.bezierPath = [[UIBezierPath alloc] init];
  16. [self.bezierPath moveToPoint:CGPointMake(0, 150)];
  17. [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
  18. //draw the path using a CAShapeLayer
  19. CAShapeLayer *pathLayer = [CAShapeLayer layer];
  20. pathLayer.path = self.bezierPath.CGPath;
  21. pathLayer.fillColor = [UIColor clearColor].CGColor;
  22. pathLayer.strokeColor = [UIColor redColor].CGColor;
  23. pathLayer.lineWidth = 3.0f;
  24. [self.containerView.layer addSublayer:pathLayer];
  25. //add the ship
  26. self.shipLayer = [CALayer layer];
  27. self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
  28. self.shipLayer.position = CGPointMake(0, 150);
  29. self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
  30. [self.containerView.layer addSublayer:self.shipLayer];
  31. //set initial values
  32. [self updateSliders];
  33. }
  34. - (IBAction)updateSliders
  35. {
  36. CFTimeInterval timeOffset = self.timeOffsetSlider.value;
  37. self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
  38. float speed = self.speedSlider.value;
  39. self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
  40. }
  41. - (IBAction)play
  42. {
  43. //create the keyframe animation
  44. CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
  45. animation.keyPath = @"position";
  46. animation.timeOffset = self.timeOffsetSlider.value;
  47. animation.speed = self.speedSlider.value;
  48. animation.duration = 1.0;
  49. animation.path = self.bezierPath.CGPath;
  50. animation.rotationMode = kCAAnimationRotateAuto;
  51. animation.removedOnCompletion = NO;
  52. [self.shipLayer addAnimation:animation forKey:@"slide"];
  53. }
  54. @end

9.2 层级关系时间

层级关系时间

9.3 手动动画

手动动画

timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。

举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。

因为在动画添加到图层之后不能再做修改了,我们来通过调整layertimeOffset达到同样的效果(清单9.4)。

清单9.4 通过触摸手势手动控制动画

  1. @interface ViewController ()
  2. @property (nonatomic, weak) UIView *containerView;
  3. @property (nonatomic, strong) CALayer *doorLayer;
  4. @end
  5. @implementation ViewController
  6. - (void)viewDidLoad
  7. {
  8. [super viewDidLoad];
  9. //add the door
  10. self.doorLayer = [CALayer layer];
  11. self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
  12. self.doorLayer.position = CGPointMake(150 - 64, 150);
  13. self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
  14. self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
  15. [self.containerView.layer addSublayer:self.doorLayer];
  16. //apply perspective transform
  17. CATransform3D perspective = CATransform3DIdentity;
  18. perspective.m34 = -1.0 / 500.0;
  19. self.containerView.layer.sublayerTransform = perspective;
  20. //add pan gesture recognizer to handle swipes
  21. UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
  22. [pan addTarget:self action:@selector(pan:)];
  23. [self.view addGestureRecognizer:pan];
  24. //pause all layer animations
  25. self.doorLayer.speed = 0.0;
  26. //apply swinging animation (which won't play because layer is paused)
  27. CABasicAnimation *animation = [CABasicAnimation animation];
  28. animation.keyPath = @"transform.rotation.y";
  29. animation.toValue = @(-M_PI_2);
  30. animation.duration = 1.0;
  31. [self.doorLayer addAnimation:animation forKey:nil];
  32. }
  33. - (void)pan:(UIPanGestureRecognizer *)pan
  34. {
  35. //get horizontal component of pan gesture
  36. CGFloat x = [pan translationInView:self.view].x;
  37. //convert from points to animation duration //using a reasonable scale factor
  38. x /= 200.0f;
  39. //update timeOffset and clamp result
  40. CFTimeInterval timeOffset = self.doorLayer.timeOffset;
  41. timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
  42. self.doorLayer.timeOffset = timeOffset;
  43. //reset pan gesture
  44. [pan setTranslation:CGPointZero inView:self.view];
  45. }
  46. @end

这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来直接设置门的transform会更简单。

在这个例子中的确是这样,但是对于比如说关键这这样更加复杂的情况,或者有多个图层的动画组,相对于实时计算每个图层的属性而言,这就显得方便的多了。

9.4 总结

总结

在这一章,我们了解了CAMediaTiming协议,以及Core Animation用来操作时间控制动画的机制。在下一章,我们将要接触缓冲,另一个用来使动画更加真实的操作时间的技术。

10. 缓冲

缓冲

生活和艺术一样,最美的永远是曲线。 — 爱德华布尔沃 - 利顿

在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制—所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如何对你的动画控制和自定义缓冲曲线。

10.1 动画速度

动画速度

动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来:

  1. velocity = change / time

这里的变化可以指的是一个物体移动的距离,时间指动画持续的时长,用这样的一个移动可以更加形象的描述(比如positionbounds属性的动画),但实际上它应用于任意可以做动画的属性(比如coloropacity)。

上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章“显式动画”的情况),对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。

考虑一个场景,一辆车行驶在一定距离内,它并不会一开始就以60mph的速度行驶,然后到达终点后突然变成0mph。一是因为需要无限大的加速度(即使是最好的车也不会在0秒内从0跑到60),另外不然的话会干死所有乘客。在现实中,它会慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下来。

那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。

现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。

CAMediaTimingFunction

那么该如何使用缓冲方程式呢?首先需要设置CAAnimation的timingFunction属性,是CAMediaTimingFunction类的一个对象。如果想改变隐式动画的计时函数,同样也可以使用CATransaction+setAnimationTimingFunction:方法。

这里有一些方式来创建CAMediaTimingFunction,最简单的方式是调用+timingFunctionWithName:的构造方法。这里传入如下几个常量之一:

  1. kCAMediaTimingFunctionLinear
  2. kCAMediaTimingFunctionEaseIn
  3. kCAMediaTimingFunctionEaseOut
  4. kCAMediaTimingFunctionEaseInEaseOut
  5. kCAMediaTimingFunctionDefault

kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimation的timingFunction属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。

kCAMediaTimingFunctionEaseIn常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。

kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。

kCAMediaTimingFunctionEaseInEaseOut创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。

最后还有一个kCAMediaTimingFunctionDefault,它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式的CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。

你可以使用一个简单的测试工程来实验一下(清单10.1),在运行之前改变缓冲函数的代码,然后点击任何地方来观察图层是如何通过指定的缓冲移动的。

清单10.1 缓冲函数的简单测试

  1. @interface ViewController ()
  2. @property (nonatomic, strong) CALayer *colorLayer;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad
  6. {
  7. [super viewDidLoad];
  8. //create a red layer
  9. self.colorLayer = [CALayer layer];
  10. self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
  11. self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0);
  12. self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
  13. [self.view.layer addSublayer:self.colorLayer];
  14. }
  15. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  16. {
  17. //configure the transaction
  18. [CATransaction begin];
  19. [CATransaction setAnimationDuration:1.0];
  20. [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
  21. //set the position
  22. self.colorLayer.position = [[touches anyObject] locationInView:self.view];
  23. //commit transaction
  24. [CATransaction commit];
  25. }
  26. @end

UIView的动画缓冲

UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:

  1. UIViewAnimationOptionCurveEaseInOut
  2. UIViewAnimationOptionCurveEaseIn
  3. UIViewAnimationOptionCurveEaseOut
  4. UIViewAnimationOptionCurveLinear

它们和CAMediaTimingFunction紧密关联,UIViewAnimationOptionCurveEaseInOut是默认值(这里没有kCAMediaTimingFunctionDefault相对应的值了)。

具体使用方法见清单10.2(注意到这里不再使用UIView额外添加的图层,因为UIKit的动画并不支持这类图层)。

清单10.2 使用UIKit动画的缓冲测试工程

  1. @interface ViewController ()
  2. @property (nonatomic, strong) UIView *colorView;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad
  6. {
  7. [super viewDidLoad];
  8. //create a red layer
  9. self.colorView = [[UIView alloc] init];
  10. self.colorView.bounds = CGRectMake(0, 0, 100, 100);
  11. self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
  12. self.colorView.backgroundColor = [UIColor redColor];
  13. [self.view addSubview:self.colorView];
  14. }
  15. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  16. {
  17. //perform the animation
  18. [UIView animateWithDuration:1.0 delay:0.0
  19. options:UIViewAnimationOptionCurveEaseOut
  20. animations:^{
  21. //set the position
  22. self.colorView.center = [[touches anyObject] locationInView:self.view];
  23. }
  24. completion:NULL];
  25. }
  26. @end

缓冲和关键帧动画

或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因(见清单8.5)看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合适的缓冲方法,例如kCAMediaTimingFunctionEaseIn,给图层的颜色变化添加一点脉冲效果,让它更像现实中的一个彩色灯泡。

我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓冲,于是每次颜色的变换都会有脉冲效果。

CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。

在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了(见清单10.3)。

运行更新后的代码,你会发现动画看起来更加自然了。

清单10.3 对CAKeyframeAnimation使用CAMediaTimingFunction

  1. @interface ViewController ()
  2. @property (nonatomic, weak) IBOutlet UIView *layerView;
  3. @property (nonatomic, weak) IBOutlet CALayer *colorLayer;
  4. @end
  5. @implementation ViewController
  6. - (void)viewDidLoad
  7. {
  8. [super viewDidLoad];
  9. //create sublayer
  10. self.colorLayer = [CALayer layer];
  11. self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
  12. self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
  13. //add it to our view
  14. [self.layerView.layer addSublayer:self.colorLayer];
  15. }
  16. - (IBAction)changeColor
  17. {
  18. //create a keyframe animation
  19. CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
  20. animation.keyPath = @"backgroundColor";
  21. animation.duration = 2.0;
  22. animation.values = @[
  23. (__bridge id)[UIColor blueColor].CGColor,
  24. (__bridge id)[UIColor redColor].CGColor,
  25. (__bridge id)[UIColor greenColor].CGColor,
  26. (__bridge id)[UIColor blueColor].CGColor ];
  27. //add timing function
  28. CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
  29. animation.timingFunctions = @[fn, fn, fn];
  30. //apply animation to layer
  31. [self.colorLayer addAnimation:animation forKey:nil];
  32. }
  33. @end

10.2 自定义缓冲函数

自定义缓冲函数

在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?

除了+functionWithName:之外,CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。

使用这个方法,我们可以创建一个自定义的缓冲函数,来匹配我们的时钟动画,为了理解如何使用这个方法,我们要了解一些CAMediaTimingFunction是如何工作的。

三次贝塞尔曲线

CAMediaTimingFunction函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。

图10.2 三次贝塞尔缓冲函数

实际上它是一个很奇怪的函数,先加速,然后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢?

CAMediaTimingFunction有一个叫做-getControlPointAtIndex:values:的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为什么不简单返回一个CGPoint),但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPathCAShapeLayer来把它画出来。

曲线的起始和终点始终是{0, 0}和{1, 1},于是我们只需要检索曲线的第二个和第三个点(控制点)。具体代码见清单10.4。所有的标准缓冲函数的图像见图10.3。

清单10.4 使用UIBezierPath绘制CAMediaTimingFunction

  1. @interface ViewController ()
  2. @property (nonatomic, weak) IBOutlet UIView *layerView;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad
  6. {
  7. [super viewDidLoad];
  8. //create timing function
  9. CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
  10. //get control points
  11. CGPoint controlPoint1, controlPoint2;
  12. [function getControlPointAtIndex:1 values:(float *)&controlPoint1];
  13. [function getControlPointAtIndex:2 values:(float *)&controlPoint2];
  14. //create curve
  15. UIBezierPath *path = [[UIBezierPath alloc] init];
  16. [path moveToPoint:CGPointZero];
  17. [path addCurveToPoint:CGPointMake(1, 1)
  18. controlPoint1:controlPoint1 controlPoint2:controlPoint2];
  19. //scale the path up to a reasonable size for display
  20. [path applyTransform:CGAffineTransformMakeScale(200, 200)];
  21. //create shape layer
  22. CAShapeLayer *shapeLayer = [CAShapeLayer layer];
  23. shapeLayer.strokeColor = [UIColor redColor].CGColor;
  24. shapeLayer.fillColor = [UIColor clearColor].CGColor;
  25. shapeLayer.lineWidth = 4.0f;
  26. shapeLayer.path = path.CGPath;
  27. [self.layerView.layer addSublayer:shapeLayer];
  28. //flip geometry so that 0,0 is in the bottom-left
  29. self.layerView.layer.geometryFlipped = YES;
  30. }
  31. @end

[图片上传失败…(image-285090-1576128672111)]
图10.4 自定义适合时钟的缓冲函数

清单10.5 添加了自定义缓冲函数的时钟程序

  1. - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
  2. {
  3. //generate transform
  4. CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
  5. if (animated) {
  6. //create transform animation
  7. CABasicAnimation *animation = [CABasicAnimation animation];
  8. animation.keyPath = @"transform";
  9. animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"];
  10. animation.toValue = [NSValue valueWithCATransform3D:transform];
  11. animation.duration = 0.5;
  12. animation.delegate = self;
  13. animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
  14. //apply animation
  15. handView.layer.transform = transform;
  16. [handView.layer addAnimation:animation forKey:nil];
  17. } else {
  18. //set transform directly
  19. handView.layer.transform = transform;
  20. }
  21. }

更加复杂的动画曲线

考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,然后经过几次反弹,最后停下来。如果用一张图来说明,它会如图10.5所示。

这可以起到作用,但效果并不是很好,到目前为止我们所完成的只是一个非常复杂的方式来使用线性缓冲复制CABasicAnimation的行为。这种方式的好处在于我们可以更加精确地控制缓冲,这也意味着我们可以应用一个完全定制的缓冲函数。那么该如何做呢?

缓冲背后的数学并不很简单,但是幸运的是我们不需要一一实现它。罗伯特·彭纳有一个网页关于缓冲函数(http://www.robertpenner.com/easing ),包含了大多数普遍的缓冲函数的多种编程语言的实现的链接,包括C。这里是一个缓冲进入缓冲退出函数的示例(实际上有很多不同的方式去实现它)。

  1. float quadraticEaseInOut(float t)
  2. {
  3. return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1;
  4. }

对我们的弹性球来说,我们可以使用bounceEaseOut函数:

  1. float bounceEaseOut(float t)
  2. {
  3. if (t < 4/11.0) {
  4. return (121 * t * t)/16.0;
  5. } else if (t < 8/11.0) {
  6. return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
  7. } else if (t < 9/10.0) {
  8. return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
  9. }
  10. return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
  11. }

如果修改清单10.7的代码来引入bounceEaseOut方法,我们的任务就是仅仅交换缓冲函数,现在就可以选择任意的缓冲类型创建动画了(见清单10.8)。

清单10.8 用关键帧实现自定义的缓冲函数

  1. - (void)animate
  2. {
  3. //reset ball to top of screen
  4. self.ballView.center = CGPointMake(150, 32);
  5. //set up animation parameters
  6. NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
  7. NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
  8. CFTimeInterval duration = 1.0;
  9. //generate keyframes
  10. NSInteger numFrames = duration * 60;
  11. NSMutableArray *frames = [NSMutableArray array];
  12. for (int i = 0; i < numFrames; i++) {
  13. float time = 1/(float)numFrames * i;
  14. //apply easing
  15. time = bounceEaseOut(time);
  16. //add keyframe
  17. [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
  18. }
  19. //create keyframe animation
  20. CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
  21. animation.keyPath = @"position";
  22. animation.duration = 1.0;
  23. animation.delegate = self;
  24. animation.values = frames;
  25. //apply animation
  26. [self.ballView.layer addAnimation:animation forKey:nil];
  27. }

10.3 总结

另外,如果你想一起进阶,不妨添加一下交流群1012951431,选择加入一起交流,一起学习。期待你的加入!
iOS核心动画高级技巧 - 5 - 图2

在这一章中,我们了解了有关缓冲和CAMediaTimingFunction类,它可以允许我们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用CAKeyframeAnimation来避开CAMediaTimingFunction的限制,创建完全自定义的缓冲函数。

在下一章中,我们将要研究基于定时器的动画—另一个给我们对动画更多控制的选择,并且实现对动画的实时操纵。