上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章 中,我们将要研究一下显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。
属性动画
CAAnimationDelegate在任何头文件中都找不到,但是可以在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,我们用- animationDidStop: finished: 方法在动画结束之后来更新图层backgroundColor的。
当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的 CABasicAnimation ,另一次是因为隐式动画,具体实现代码如下。
-(void)testBasicAnimation{
CABasicAnimation *basicAnimation = [CABasicAnimation animation];
basicAnimation.keyPath = @"backgroundColor";
basicAnimation.toValue = (__bridge id)[UIColor redColor].CGColor;
basicAnimation.delegate = self;
basicAnimation.duration = 1;
[basicAnimation setValue:@"123" forKey:@"123"];
[self.layer1 addAnimation:basicAnimation forKey:@"123"];
NSLog(@"basicAnimation:%@",basicAnimation);
}
/// 动画结束后的代理方法
/// @param anim 动画对象
/// @param flag 是否完成
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
//这个地方跟上面的basicAnimation不是同一个对象,是深拷贝过来的
CAAnimation *an = [self.layer1 animationForKey:@"123"];
NSArray *arr = [self.layer1 animationKeys];
NSString *str = [anim valueForKey:@"123"];
NSLog(@"anim:%@ --- %@ --- %@ -- %@ -- %@",anim,an,arr,self.layer1,str);
//打印:anim: --- (null) --- (null) -- -- 123
//这样就有两个动画了
[CATransaction begin];
// [CATransaction setDisableActions:true];
[CATransaction setAnimationDuration:2];
// self.layer1.backgroundColor = (__bridge CGColorRef)anim.toValue;
self.layer1.backgroundColor = [UIColor orangeColor].CGColor;
[CATransaction commit];
}
对 CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当 你有多个动画的时候,无法在在回调方法中区分。在一个视图控制器中创建动画的 时候,通常会用控制器本身作为一个委托(如上面所示),但是所有的动画都会调用同一个回调方法,所以你就需要判断到底是那个图层的调用
虽然layer提供了-animation:forKey:把动画添加到图层,也提供了了-animationForKey: 方法找到对应动 画的唯一标识符,但是通过测试是没用的,所以我们换一个方法:
像所有的 NSObject子类一 样,CAAnimation实现了KVC(键-值-编码)协议,于是你可以用 - setValue:forKey:和- valueForKey:方法来存取属性。但是CAAnimation有 一个不同的性能:它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。
这意味着你可以对动画用任意类型打标签。在这里,我们给UIView类型的指针添 加的动画,所以可以简单地判断动画到底属于哪个视图,然后在委托方法中用这个 信息正确地更新钟的指针。
在模拟器上运行的很好,但当真 正跑在iOS设备上时,我们发现在 -animationDidStop:finished: 委托方法调用 之前,指针会迅速返回到原始值。
问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返
回初始状态之前。这同时也很好地说明了为什么要在真实的设备上测试动画代码,
而不仅仅是模拟器。
我们可以用一个 fillMode 属性来解决这个问题,下一章会详细说明,这里知道在 动画之前设置它比在动画结束之后更新属性更加方便
关键帧动画
CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确很有趣,但是显示地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。
CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation 类似, CAKeyframeAnimation 同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation 不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就 是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)将由熟练的艺术家来 完成。 CAKeyframeAnimation 也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插入。
我们可以用之前使用颜色图层的例子来演示,设置一个颜色的数组,然后通过关键 帧动画播放出来
-(void)testKeyframeAnimation{
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];
[self.layer1 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,然后用它来应用到我们的宇宙飞船。
-(void)testCGPathKeyFrameAnimation{
UIBezierPath *bezierPath = [[UIBezierPath alloc]init];
[bezierPath moveToPoint:CGPointMake(0, kWindowHeight/2)];
[bezierPath addCurveToPoint:CGPointMake(300, kWindowHeight/2) controlPoint1:CGPointMake(200, kWindowHeight/2 + 100) controlPoint2:CGPointMake(150, 50)];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3;
[self.view.layer addSublayer:pathLayer];
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 64, 64);
shipLayer.position = CGPointMake(0, kWindowHeight/2);
shipLayer.contents = (__bridge id)[UIImage imageNamed:@"AppIcon60x60"].CGImage;
[self.view.layer addSublayer:shipLayer];
CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animation];
keyAnimation.keyPath = @"position";
keyAnimation.duration = 4;
keyAnimation.path = bezierPath.CGPath;
/*
kCAAnimationRotateAuto:中心线跟path在一条线上
kCAAnimationRotateAutoReverse:物体反过来做动画
如果不设置,只保证中心点在轨迹上
*/
keyAnimation.rotationMode = kCAAnimationRotateAutoReverse;
[shipLayer addAnimation:keyAnimation forKey:nil];
}
运行示例,你会发现飞船的动画有些不太真实,这是因为当它运动的时候永远指向 右边,而不是指向曲线切线的方向。你可以调整它的 affineTransform 来对运动 方向做动画,但很可能和其它的动画冲突。
苹果预见到了这点,并且给CAKeyFrameAnimation添加了一个rotationMode 的属性。设置它为常量KCAAnimationRotateAuto,图层将会根据曲线的切线自动旋转。
虚拟属性
之前提到过属性动画实际上是针对于关键路径而不是一个键,这就意味着可以对子属性甚至是虚拟属性做动画。但是虚拟属性到底是什么呢?
考虑一个旋转的动画:如果想要对一个物体做旋转的动画,那就需要作用
于transform 属性,因为 CALayer没有显式提供角度或者方向之类的属性,代 码如下所示
用 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 用于把我们赋给虚拟的transfrom.rotation 简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。你可以通过设置CAPropertyAnimation的 valueFunction属性来改变,于是你设置的函数将会覆盖默认的函数。
CAValueFunction 看起来似乎是对那些不能简单相加的属性(例如变换矩阵)做动画的非常有用的机制,但由于 CAValueFunction 的实现细节是私有的,所以目 前不能通过继承它来自定义。你可以通过使用苹果目前已经提供的常量(目前都是 和变换矩阵的虚拟属性相关,所以没太多使用场景了,因为这些属性都有了默认的 实现方式)。