若是想让事情变得顺利,只有靠本身 -- 夏尔·纪尧姆 html
上一章介绍了隐式动画的概念。隐式动画是在iOS平台建立动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖全部的动画类型。在这一章中,咱们将要研究一下显式动画,它可以对一些属性作指定的自定义动画,或者建立非线性动画,好比沿着任意一条曲线移动。 git
首先咱们来探讨一下属性动画。属性动画做用于图层的某个单一属性,并指定了它的一个目标值,或者一连串将要作动画的值。属性动画分为两种:基础和关键帧。 github
动画其实就是一段时间内发生的改变,最简单的形式就是从一个值改变到另外一个值,这也是 CABasicAnimation 最主要的功能。 CABasicAnimation 是 CAPropertyAnimation 的一个子类,而 CAPropertyAnimation 的父类是 CAAnimation , CAAnimation 同时也是Core Animation全部动画类型的抽象基类。做为一个抽象类,CAAnimation自己并无作多少工做,它提供了一个计时函数(见第十章“缓冲”),一个委托(用于反馈动画状态)以及一个 removedOnCompletion ,用于标识动画是否该在结束后自动释放(默认YES,为了防止内存泄露)。CAAnimation同时实现了一些协议,包括CAAction(容许CAAnimation的子类能够提供图层行为),以及 CAMediaTiming (第九章“图层时间”将会详细解释)。 安全
CAPropertyAnimation经过指定动画的 keyPath 做用于一个单一属性,CAAnimation一般应用于一个指定的CALayer,因而这里指的也就是一个图层的keyPath了。实际上它是一个关键路径(一些用点表示法能够在层级关系中指向任意嵌套的对象),而不只仅是一个属性的名称,由于这意味着动画不只能够做用于图层自己的属性,并且还包含了它的子成员的属性,甚至是一些虚拟的属性(后面会详细解释)。 app
CABasicAnimation继承于CAPropertyAnimation,并添加了以下属性: dom
id fromValue; id toValue; id byValue;
从命名就能够获得很好的解释:fromValue表明了动画开始以前属性的值,toValue表明了动画结束以后的值,byValue表明了动画执行过程当中改变的值。 函数
经过组合这三个属性就能够有不少种方式来指定一个动画的过程。它们被定义成id类型而不是一些具体的类型是由于属性动画能够用做不少不一样种的属性类型,包括数字类型,矢量,变换矩阵,甚至是颜色或者图片。 性能
id类型能够包含任意由NSObject派生的对象,但有时候你会但愿对一些不直接从NSObject继承的属性类型作动画,这意味着你须要把这些值用一个对象来封装,或者强转成一个对象,就像某些功能和Objective-C对象相似的Core Foundation类型。可是如何从一个具体的数据类型转换成id看起来并不明显,一些普通的例子见表8.1。 测试
表8.1 用于CAPropertyAnimation的一些类型转换 动画
Type | Object Type | Code Example |
---|---|---|
CGFloat | NSNumber | id obj = @(float); |
CGPoint | NSValue | id obj = [NSValue valueWithCGPoint:point); |
CGSize | NSValue | id obj = [NSValue valueWithCGSize:size); |
CGRect | NSValue | id obj = [NSValue valueWithCGRect:rect); |
CATransform3D | NSValue | id obj = [NSValue valueWithCATransform3D:transform); |
CGImageRef | id | id obj = (__bridge id)imageRef; |
CGColorRef | id | id obj = (__bridge id)colorRef; |
fromValue , toValue 和 byValue 属性能够用不少种方式来组合,但为了防止冲突,不能一次性同时指定这三个值。例如,若是指定了fromValue等于2,toValue等于4,byValue等于3,那么Core Animation就不知道结果究竟是4(toValue)仍是5(fromValue + byValue)了。他们的用法在CABasicAnimation头文件中已经描述的很清楚了,因此在这里就不重复了。总的说来,就是只须要指定toValue或者byValue,剩下的值均可以经过上下文自动计算出来。
举个例子:咱们修改一下第七章中的颜色渐变的动画,用显式的CABasicAnimation来取代以前的隐式动画,代码见清单8.1。
清单8.1 经过CABasicAnimation来设置图层背景色
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, strong) IBOutlet CALayer *colorLayer; @end @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; //apply animation to layer [self.colorLayer addAnimation:animation forKey:nil]; } @end
运行程序,结果有点差强人意,点击按钮,的确可使图层动画过渡到一个新的颜色,然动画结束以后又马上变回原始值。
这是由于动画并无改变图层的模型,而只是呈现(第七章)。一旦动画结束并从图层上移除以后,图层就马上恢复到以前定义的外观状态。咱们从没改变过backgroundColor属性,因此图层就返回到原始的颜色。
当以前在使用隐式动画的时候,实际上它就是用例子中CABasicAnimation来实现的(回忆第七章,咱们在-actionForLayer:forKey:委托方法打印出来的结果就是CABasicAnimation)。可是在那个例子中,咱们经过设置属性来打开动画。在这里咱们作了相同的动画,可是并无设置任何属性的值(这就是为何会马上变回初始状态的缘由)。
把动画设置成一个图层的行为(而后经过改变属性值来启动动画)是到目前为止同步属性值和动画状态最简单的方式了,假设因为某些缘由咱们不能这么作(一般由于UIView关联的图层不能这么作动画),那么有两种能够更新属性值的方式:在动画开始以前或者动画结束以后。
动画以前改变属性的值是最简单的办法,但这意味着咱们不能使用fromValue这么好的特性了,并且要手动将fromValue设置成图层当前的值。
因而在动画建立以前插入以下代码,就能够解决问题了
animation.fromValue = (__bridge id)self.colorLayer.backgroundColor; self.colorLayer.backgroundColor = color.CGColor;
这的确是可行的,但仍是有些问题,若是这里已经正在进行一段动画,咱们须要从呈现图层那里去得到fromValue,而不是模型图层。另外,因为这里的图层并非UIView关联的图层,咱们须要用CATransaction来禁用隐式动画行为,不然默认的图层行为会干扰咱们的显式动画(实际上,显式动画一般会覆盖隐式动画,但在文章中并无提到,因此为了安全最好这么作)。
更新以后的代码以下:
CALayer *layer = self.colorLayer.presentationLayer ?: self.colorLayer; animation.fromValue = (__bridge id)layer.backgroundColor; [CATransaction begin]; [CATransaction setDisableActions:YES]; self.colorLayer.backgroundColor = color.CGColor; [CATransaction commit];
若是给每一个动画都添加这些,代码会显得特别臃肿。幸运的是,咱们能够从CABasicAnimation去自动设置这些。因而能够建立一个可复用的代码。清单8.2修改了以前的示例,经过使用CABasicAnimation的一个函数来避免在每次动画时候都重复那些臃肿的代码。
清单8.2 修改动画马上恢复到原始状态的一个可复用函数
- (void)applyBasicAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer { //set the from value (using presentation layer if available) animation.fromValue = [layer.presentationLayer ?: layer valueForKeyPath:animation.keyPath]; //update the property in advance //note: this approach will only work if toValue != nil [CATransaction begin]; [CATransaction setDisableActions:YES]; [layer setValue:animation.toValue forKeyPath:animation.keyPath]; [CATransaction commit]; //apply animation to layer [layer addAnimation:animation forKey:nil]; } - (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; //apply animation without snap-back [self applyBasicAnimation:animation toLayer:self.colorLayer]; }
这种简单的实现方式经过toValue而不是byValue来处理动画,不过这已是朝更好的解决方案迈出一大步了。你能够把它添加给CALayer做为一个分类,以方便更好地使用。
解决看起来如此简单的一个问题都着实麻烦,可是别的方案会更加复杂。若是不在动画开始以前去更新目标属性,那么就只能在动画彻底结束或者取消的时候更新它。这意味着咱们须要精准地在动画结束以后,图层返回到原始值以前更新属性。那么该如何找到这个点呢?
在第七章使用隐式动画的时候,咱们能够在CATransaction完成块中检测到动画的完成。可是这种方式并不适用于显式动画,由于这里的动画和事务并没太多关联。
那么为了知道一个显式动画在什么时候结束,咱们须要使用一个实现了CAAnimationDelegate协议的delegate。
CAAnimationDelegate在任何头文件中都找不到,可是能够在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,咱们用-animationDidStop:finished:方法在动画结束以后来更新图层的backgroundColor。
当更新属性的时候,咱们须要设置一个新的事务,而且禁用图层行为。不然动画会发生两次,一个是由于显式的CABasicAnimation,另外一次是由于隐式动画,具体实现见订单8.3。
清单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属性来解决这个问题,下一章会详细说明,这里知道在动画以前设置它比在动画结束以后更新属性更加方便。