若是想让事情变得顺利,只有靠本身 -- 夏尔·纪尧姆面试
上一章介绍了隐式动画的概念。隐式动画是在iOS平台建立动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖全部的动画类型。在这一章中,咱们将要研究一下显式动画,它可以对一些属性作指定的自定义动画,或者建立非线性动画,好比沿着任意一条曲线移动。数组
CAAnimationDelegate
在任何头文件中都找不到,可是能够在CAAnimation
头文件或者苹果开发者文档中找到相关函数。在这个例子中,咱们用-animationDidStop:finished:
方法在动画结束以后来更新图层的backgroundColor
。app
当更新属性的时候,咱们须要设置一个新的事务,而且禁用图层行为。不然动画会发生两次,一个是由于显式的CABasicAnimation
,另外一次是由于隐式动画,具体实现见订单8.3。dom
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。函数
清单8.3 动画完成以后修改图层的背景色布局
@implementation ViewController - (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 it to our view [self.layerView.layer addSublayer:self.colorLayer]; } - (IBAction)changeColor { //create a new random color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; //create a basic animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"backgroundColor"; animation.toValue = (__bridge id)color.CGColor; animation.delegate = self; //apply animation to layer [self.colorLayer addAnimation:animation forKey:nil]; } - (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]; } @end
对CAAnimation
而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,没法在在回调方法中区分。在一个视图控制器中建立动画的时候,一般会用控制器自己做为一个委托(如清单8.3所示),可是全部的动画都会调用同一个回调方法,因此你就须要判断究竟是那个图层的调用。性能
考虑一下第三章的闹钟,“图层几何学”,咱们经过简单地每秒更新指针的角度来实现一个钟,但若是指针动态地转向新的位置会更加真实。学习
咱们不能经过隐式动画来实现由于这些指针都是UIView
的实例,因此图层的隐式动画都被禁用了。咱们能够简单地经过UIView
的动画方法来实现。但若是想更好地控制动画时间,使用显式动画会更好(更多内容见第十章)。使用CABasicAnimation
来作动画可能会更加复杂,由于咱们须要在-animationDidStop:finished:
中检测指针状态(用于设置结束的位置)。测试
动画自己会做为一个参数传入委托的方法,也许你会认为能够控制器中把动画存储为一个属性,而后在回调用比较,但实际上并不起做用,由于委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。动画
当使用-addAnimation:forKey:
把动画添加到图层,这里有一个到目前为止咱们都设置为nil
的key参数。这里的键是-animationForKey:
方法找到对应动画的惟一标识符,而当前动画的全部键均可以用animationKeys
获取。若是咱们对每一个动画都关联一个惟一的键,就能够对每一个图层循环全部键,而后调用-animationForKey:
来比对结果。尽管这不是一个优雅的实现。
幸运的是,还有一种更加简单的方法。像全部的NSObject
子类同样,CAAnimation
实现了KVC(键-值-编码)协议,因而你能够用-setValue:forKey:
和-valueForKey:
方法来存取属性。可是CAAnimation
有一个不一样的性能:它更像一个NSDictionary
,可让你随意设置键值对,即便和你使用的动画类所声明的属性并不匹配。
这意味着你能够对动画用任意类型打标签。在这里,咱们给UIView
类型的指针添加的动画,因此能够简单地判断动画到底属于哪一个视图,而后在委托方法中用这个信息正确地更新钟的指针(清单8.4)。
清单8.4 使用KVC对动画打标签
@interface ViewController () @property (nonatomic, weak) IBOutlet UIImageView *hourHand; @property (nonatomic, weak) IBOutlet UIImageView *minuteHand; @property (nonatomic, weak) IBOutlet UIImageView *secondHand; @property (nonatomic, weak) NSTimer *timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //adjust anchor points self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); //start timer self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; //set initial hand positions [self updateHandsAnimated:NO]; } - (void)tick { [self updateHandsAnimated:YES]; } - (void)updateHandsAnimated:(BOOL)animated { //convert time to hours, minutes and seconds NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; NSDateComponents *components = [calendar components:units fromDate:[NSDate date]]; CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0; //calculate hour hand angle //calculate minute hand angle CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0; //calculate second hand angle CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0; //rotate hands [self setAngle:hourAngle forHand:self.hourHand animated:animated]; [self setAngle:minuteAngle forHand:self.minuteHand animated:animated]; [self setAngle:secondAngle forHand:self.secondHand animated:animated]; } - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated { //generate transform CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1); if (animated) { //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]; } else { //set transform directly handView.layer.transform = transform; } } - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag { //set final position for hand view UIView *handView = [anim valueForKey:@"handView"]; handView.layer.transform = [anim.toValue CATransform3DValue]; }
咱们成功的识别出每一个图层中止动画的时间,而后更新它的变换到一个新值,很好。
不幸的是,即便作了这些,仍是有个问题,清单8.4在模拟器上运行的很好,但当真正跑在iOS设备上时,咱们发如今-animationDidStop:finished:
委托方法调用以前,指针会迅速返回到原始值,这个清单8.3图层颜色发生的状况同样。
问题在于回调方法在动画完成以前已经被调用了,但不能保证这发生在属性动画返回初始状态以前。这同时也很好地说明了为何要在真实的设备上测试动画代码,而不只仅是模拟器。
咱们能够用一个fillMode
属性来解决这个问题,下一章会详细说明,这里知道在动画以前设置它比在动画结束以后更新属性更加方便。
CABasicAnimation
揭示了大多数隐式动画背后依赖的机制,这的确颇有趣,可是显式地给图层添加CABasicAnimation
相较于隐式动画而言,只能说费力不讨好。
CAKeyframeAnimation
是另外一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation
相似,CAKeyframeAnimation
一样是CAPropertyAnimation
的一个子类,它依然做用于单一的一个属性,可是和CABasicAnimation
不同的是,它不限制于设置一个起始和结束的值,而是能够根据一连串随意的值来作动画。
关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(能够经过关键帧推算出)将由熟练的艺术家来完成。CAKeyframeAnimation
也是一样的道理:你提供了显著的帧,而后Core Animation在每帧之间进行插入。
咱们能够用以前使用颜色图层的例子来演示,设置一个颜色的数组,而后经过关键帧动画播放出来(清单8.5)
清单8.5 使用CAKeyframeAnimation
应用一系列颜色的变化
- (IBAction)changeColor { //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]; }
注意到序列中开始和结束的颜色都是蓝色,这是由于CAKeyframeAnimation
并不能自动把当前值做为第一帧(就像CABasicAnimation
那样把fromValue
设为nil
)。动画会在开始的时候忽然跳转到第一帧的值,而后在动画结束的时候忽然恢复到原始的值。因此为了动画的平滑特性,咱们须要开始和结束的关键帧来匹配当前属性的值。
固然能够建立一个结束和开始值不一样的动画,那样的话就须要在动画启动以前手动更新属性和最后一帧的值保持一致,就和以前讨论的同样。
咱们用duration
属性把动画时间从默认的0.25秒增长到2秒,以便于动画作的不那么快。运行它,你会发现动画经过颜色不断循环,但效果看起来有些奇怪。缘由在于动画以一个恒定的步调在运行。当在每一个动画之间过渡的时候并无减速,这就产生了一个略微奇怪的效果,为了让动画看起来更天然,咱们须要调整一下缓冲,第十章将会详细说明。
提供一个数组的值就能够按照颜色变化作动画,但通常来讲用数组来描述动画运动并不直观。CAKeyframeAnimation
有另外一种方式去指定动画,就是使用CGPath
。path
属性能够用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。
咱们来用一个宇宙飞船沿着一个简单曲线的实例演示一下。为了建立路径,咱们须要使用一个三次贝塞尔曲线,它是一种使用开始点,结束点和另外两个控制点来定义形状的曲线,能够经过使用一个基于C的Core Graphics绘图指令来建立,不过用UIKit提供的UIBezierPath
类会更简单。
咱们此次用CAShapeLayer
来在屏幕上绘制曲线,尽管对动画来讲并非必须的,但这会让咱们的动画更加形象。绘制完CGPath
以后,咱们用它来建立一个CAKeyframeAnimation
,而后用它来应用到咱们的宇宙飞船。代码见清单8.6,结果见图8.1。
清单8.6 沿着一个贝塞尔曲线对图层作动画
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //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.containerView.layer addSublayer:pathLayer]; //add the ship CALayer *shipLayer = [CALayer layer]; shipLayer.frame = CGRectMake(0, 0, 64, 64); shipLayer.position = CGPointMake(0, 150); shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; [self.containerView.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]; } @end
这么作是可行的,但看起来更由于是运气而不是设计的缘由,若是咱们把旋转的值从M_PI
(180度)调整到2 * M_PI
(360度),而后运行程序,会发现这时候飞船彻底不动了。这是由于这里的矩阵作了一次360度的旋转,和作了0度是同样的,因此最后的值根本没变。
如今继续使用M_PI
,但此次用byValue
而不是toValue
。也许你会认为这和设置toValue
结果同样,由于0 + 90度 == 90度,但实际上飞船的图片变大了,并无作任何旋转,这是由于变换矩阵不能像角度值那样叠加。
那么若是须要独立于角度以外单独对平移或者缩放作动画呢?因为都须要咱们来修改transform
属性,实时地从新计算每一个时间点的每一个变换效果,而后根据这些建立一个复杂的关键帧动画,这一切都是为了对图层的一个独立作一个简单的动画。
幸运的是,有一个更好的解决方案:为了旋转图层,咱们能够对transform.rotation
关键路径应用动画,而不是transform
自己(清单8.9)。
清单8.9 对虚拟的transform.rotation
属性作动画
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the ship CALayer *shipLayer = [CALayer layer]; shipLayer.frame = CGRectMake(0, 0, 128, 128); shipLayer.position = CGPointMake(150, 150); shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; [self.containerView.layer addSublayer:shipLayer]; //animate the ship rotation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation"; animation.duration = 2.0; animation.byValue = @(M_PI * 2); [shipLayer addAnimation:animation forKey:nil]; } @end
结果运行的特别好,用transform.rotation
而不是transform
作动画的好处以下:
咱们能够不经过关键帧一步旋转多于180度的动画。
能够用相对值而不是绝对值旋转(设置byValue
而不是toValue
)。
能够不用建立CATransform3D
,而是使用一个简单的数值来指定角度。
不会和transform.position
或者transform.scale
冲突(一样是使用关键路径来作独立的动画属性)。
transform.rotation
属性有一个奇怪的问题是它其实并不存在。这是由于CATransform3D
并非一个对象,它其实是一个结构体,也没有符合KVC
相关属性,transform.rotation其实是一个CALayer
用于处理动画变换的虚拟属性。
你不能够直接设置transform.rotation
或者transform.scale
,他们不能被直接使用。当你对他们作动画时,Core Animation自动地根据经过CAValueFunction
来计算的值来更新transform
属性。
CAValueFunction
用于把咱们赋给虚拟的transform.rotation
简单浮点值转换成真正的用于摆放图层的CATransform3D
矩阵值。你能够经过设置CAPropertyAnimation
的valueFunction
属性来改变,因而你设置的函数将会覆盖默认的函数。
CAValueFunction
看起来彷佛是对那些不能简单相加的属性(例如变换矩阵)作动画的很是有用的机制,但因为CAValueFunction
的实现细节是私有的,因此目前不能经过继承它来自定义。你能够经过使用苹果目前已近提供的常量(目前都是和变换矩阵的虚拟属性相关,因此没太多使用场景了,由于这些属性都有了默认的实现方式)。
CABasicAnimation
和CAKeyframeAnimation
仅仅做用于单独的属性,而CAAnimationGroup
能够把这些动画组合在一块儿。CAAnimationGroup
是另外一个继承于CAAnimation
的子类,它添加了一个animations数组的属性,用来组合别的动画。咱们把清单8.6那种关键帧动画和调整图层背景色的基础动画组合起来(清单8.10),结果如图8.3所示。
清单8.10 组合关键帧动画和基础动画
- (void)viewDidLoad { [super viewDidLoad]; //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.containerView.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.containerView.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]; }
有时候对于iOS应用程序来讲,但愿能经过属性动画来对比较难作动画的布局进行一些改变。好比交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起做用,因此若是要改变一个不能动画的属性(好比图片),或者从层级关系中添加或者移除图层,属性动画将不起做用。
因而就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间作动画,而是影响到整个图层的变化。过渡动画首先展现以前的图层外观,而后经过一个交换过渡到新的外观。
为了建立一个过渡动画,咱们将使用CATransition
,一样是另外一个CAAnimation
的子类,和别的子类不一样,CATransition
有一个type和subtype来标识变换效果。type属性是一个NSString
类型,能够被设置成以下类型:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
到目前为止你只能使用上述四种类型,但你能够经过一些别的方法来自定义过渡效果,后续会详细介绍。
默认的过渡类型是kCATransitionFade
,当你在改变图层属性以后,就建立了一个平滑的淡入淡出效果。
咱们在第七章的例子中就已经用到过kCATransitionPush
,它建立了一个新的图层,从边缘的一侧滑动进来,把旧图层从另外一侧推出去的效果。
kCATransitionMoveIn
和kCATransitionReveal
与kCATransitionPush
相似,都实现了一个定向滑动的动画,可是有一些细微的不一样,kCATransitionMoveIn
从顶部滑动进入,但不像推送动画那样把老土层推走,然而kCATransitionReveal
把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。
后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,可是你能够经过subtype来控制它们的方向,提供了以下四种类型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
一个简单的用CATransition
来对非动画属性作动画的例子如清单8.11所示,这里咱们对UIImage的image
属性作修改,可是隐式动画或者CAPropertyAnimation
都不能对它作动画,由于Core Animation不知道如何在插图图片。经过对图层应用一个淡入淡出的过渡,咱们能够忽略它的内容来作平滑动画(图8.4),咱们来尝试修改过渡的type
常量来观察其它效果。
清单8.11 使用CATransition
来对UIImageView
作动画
@interface ViewController () @property (nonatomic, weak) IBOutlet UIImageView *imageView; @property (nonatomic, copy) NSArray *images; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //set up images self.images = @[[UIImage imageNamed:@"Anchor.png"], [UIImage imageNamed:@"Cone.png"], [UIImage imageNamed:@"Igloo.png"], [UIImage imageNamed:@"Spaceship.png"]]; } - (IBAction)switchImage { //set up crossfade transition CATransition *transition = [CATransition animation]; transition.type = kCATransitionFade; //apply transition to imageview backing layer [self.imageView.layer addAnimation:transition forKey:nil]; //cycle to next image UIImage *currentImage = self.imageView.image; NSUInteger index = [self.images indexOfObject:currentImage]; index = (index + 1) % [self.images count]; self.imageView.image = self.images[index]; } @end
你能够从代码中看出,过渡动画和以前的属性动画或者动画组添加到图层上的方式一致,都是经过-addAnimation:forKey:
方法。可是和属性动画不一样的是,对指定的图层一次只能使用一次CATransition
,所以,不管你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”
,也就是常量kCATransition
。
以前提到过,你能够用-addAnimation:forKey:
方法中的key参数来在添加动画以后检索一个动画,使用以下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但并不支持在动画运行过程当中修改动画,因此这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。
为了终止一个指定的动画,你能够用以下方法把它从图层移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除全部动画:
- (void)removeAllAnimations;
动画一旦被移除,图层的外观就马上更新到当前的模型图层的值。通常说来,动画在结束以后被自动移除,除非设置removedOnCompletion
为NO
,若是你设置动画在结束以后不被自动移除,那么当它不须要的时候你要手动移除它;不然它会一直存在于内存中,直到图层被销毁。
咱们来扩展以前旋转飞船的示例,这里添加一个按钮来中止或者启动动画。这一次咱们用一个非nil的值做为动画的键,以便以后能够移除它。-animationDidStop:finished:
方法中的flag
参数代表了动画是天然结束仍是被打断,咱们能够在控制台打印出来。若是你用中止按钮来终止动画,它会打印NO
,若是容许它完成,它会打印YES
。
清单8.15是更新后的示例代码,图8.6显示告终果。
清单8.15 开始和中止一个动画
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (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.containerView.layer addSublayer:self.shipLayer]; } - (IBAction)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"]; } - (IBAction)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"); } @end
这一章中,咱们涉及了属性动画(你能够对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过分(影响整个图层,能够用来对图层的任何内容作任何类型的动画,包括子图层的添加和移除)。
在第九章中,咱们继续学习CAMediaTiming
协议,来看一看Core Animation是怎样处理逝去的时间。