显式动画
1. 属性动画
属性动画作用于图层的某一个单一的属性, 并指定它的目标值, 或者一连串要做动画的值. 属性动画分为基础动画
和关键帧动画
.
1.1 基础动画
动画就是一段时间内发生的改变, 最简单的形式就是从一个值改变到另一个值, 这也是CABaseAnimation
的最主要功能. CABaseAnimation是CAPropertyAnimation的一个子类, 而CAPropertyAnimation的父类是CAAnimation, CAAnimation
同时也是CoreAnimation所有动画类型的抽象基类. 作为抽象类, 事实上CAAnimation并没有完成太多工作.
CAPropertyAnimation通过制定动画的keyPath作用于一个单一属性, CAAnimation通常作用于一个指定的CALayer, 于是这里的keyPath指的是一个图层的路径了. 实际上它是关键路径而不仅仅是一个属性的名称, 就是说, 动画不仅仅可以作用于图层本身的属性, 而且还包含了它的子成员的属性, 甚至包含虚拟属性.
CABaseAnimation继承自CAPropertyAnimation, 并添加了如下三个属性:
// 动画开始之前属性的值
@property(nullable, strong) id fromValue;
// 动画结束之后属性的值
@property(nullable, strong) id toValue;
// 动画执行过程中, 改变的值
@property(nullable, strong) id byValue;
fromValue, toValue, byValue可以用多种不同的方式进行组合, 但是为了防止冲突, 不能一次性指定三个值. 需要注意的是, 如果是图片必须转换成CGImageRef类型桥接成id类型; 如果是颜色也必须转换成CGColorRef类型桥接成id类型 (__bridge id)
.
一个简单的改变背景颜色的基础动画(实际上如果是单独的layer, 用隐式动画比较好):
// 基础动画, 改变颜色
- (void)changeColor {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 70, 80, 80)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
animation.toValue = (__bridge id)[UIColor purpleColor].CGColor;
animation.duration = 10;
[view.layer addAnimation:animation forKey:nil];
}
如上代码, 存在问题是, 当动画结束之后图层颜色马上变回原来的颜色了, 原因是做动画时候的图层是呈现层, 动画结束之后呈现层被移除, 所以颜色又变回原来的颜色了. 一种解决方案如下:
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 70, 80, 80)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
animation.fromValue = (__bridge id) view.layer.backgroundColor;
animation.toValue = (__bridge id)[UIColor purpleColor].CGColor;
animation.duration = 3;
[view.layer addAnimation:animation forKey:nil];
view.backgroundColor = [UIColor purpleColor];
1.2 CAAnimationDelegate
第七章的隐式动画中, 可以用CATransaction完成块中检测动画的完成, 但是在显式动画中显然是不可能的, 因为这里的动画和事务并没有鸟关联.
在CAAnimationDelegate的代理方法中, 我们可以获取动画的开始和结束.
如下:
/* Called when the animation begins its active duration. */
- (void)animationDidStart:(CAAnimation *)anim {
NSLog(@"animation start");
}
/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
NSLog(@"animation finish");
}
我们不能通过隐式动画来实现UIView的实例, 因为所有图层的隐式动画都被禁用了. 我们可以通过简单的UIView动画实现简单的动画效果, 但是如果想要更好的控制动画时间, 使用显式动画比较好.
动画的代理方法中, 动画本身会作为一个参数传入委托方法中, 但是需要注意: 委托传入的动画参数是原始值的一个深拷贝, 而不是同一个值.
事实上, 在项目中我们用来唯一标识一个动画的方法是使用key值, 一般我们通过 -setValue:forKey:
和 -valueForKey:
这两个方法来存取. 但是CAAnimation有一个不提欧宁的性能, 它更像一个NSDictionary, 计时你使用动画类型所声明的属性并不匹配, 你也可以随意的使用键值对.
1.2 关键帧动画
CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类. 它也是CAPropertyAnimation的一个子类, 也作用于单一的一个属性, 但是它可以根据已连串随意的值来做动画.
关键帧起源于传动动画, 意思是指主导动画在显著改变发生时重绘当前帧(也就是关键帧), 没帧之间剩下的绘制通过关键帧推算出来. CAKeyFrameAnimation也是同样的道理, 你提供了显著帧之后, CoreAnimation在每帧之间进行插入.
关键帧动画, 可以提供一个数组, 数组中包含一组相关属性的值, 然后CoreAnimation会将这些值以动画的形式展示出来, 两个值之间的值CoreAnimation会计算出来, 但是, 关键帧动画开始执行时候会直接跳转到第一帧的值, 而并不会将当前的值平滑过渡到第一帧, 这时候, 我们一般需要将第一帧设置为当前颜色值相同的值. 同样的最后一帧也要和最终的颜色想等, 不然会有跳跃.
// 2. 关键帧动画, 使用颜色数组改变颜色
- (void)keyFrameAnimationChangeColor {
CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"backgroundColor"];
keyAnimation.duration = 7;
keyAnimation.values = @[(__bridge id)self.colorView.backgroundColor.CGColor,
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor blackColor].CGColor,
(__bridge id)[UIColor purpleColor].CGColor,
(__bridge id)[UIColor cyanColor].CGColor,
(__bridge id)[UIColor yellowColor].CGColor];
[self.colorView.layer addAnimation:keyAnimation forKey:nil];
// 配置完动画后, 更改view颜色, 避免动画结束之后颜色突然跳跃式变化回原来颜色.
self.colorView.layer.backgroundColor = [UIColor yellowColor].CGColor;
}
一般来说, 通过数组描述动画的运动并不直观. CAKeyFrameAnimation可以用另一种方式指定动画 CGPath
. path属性可以用一种直观的方式, 使用CoreGraphics函数定义运动序列来绘制动画路径.
// 3. 关键帧动画,
- (void)pathAnimation {
// 使用贝塞尔画曲线
UIBezierPath *bPath = [UIBezierPath bezierPath];
[bPath moveToPoint:CGPointMake(30, 250)];
[bPath addCurveToPoint:CGPointMake(350, 250) controlPoint1:CGPointMake(100, 150) controlPoint2:CGPointMake(250, 400)];
// 将曲线添加到shapeLayer上面
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = bPath.CGPath;
shapeLayer.lineWidth = 0.5;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:shapeLayer];
//创建飞机控件
UIImageView *plane = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
plane.center = CGPointMake(30, 150);
plane.backgroundColor = [UIColor blueColor];
[self.view addSubview:plane];
// 创建path动画
CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
keyAnimation.path = bPath.CGPath;
keyAnimation.duration = 3.f;
// 设置图形方向跟随曲线切面方向自动做旋转动作.
keyAnimation.rotationMode = kCAAnimationRotateAuto;
[plane.layer addAnimation:keyAnimation forKey:nil];
}
**效果: **
1.3 虚拟属性
属性动画针对的是一个键, 也就意味着可以对子属性甚至虚拟属性做动画. 例如, 如果需要用到旋转则需要使用 transform
属性, 但是使用transform直接旋转2*M_PI, 实际上不会做任何变化, 使用起来还是非常不方便的, 这时候可以使用transform的虚拟属性. transform.rotation
, 创建CABaseAnimation动画, 设置keypath是 trabsfirm.rotation
, 设置byValue, 就可以直接对图层进行旋转操作, 比使用 CATransform3D方便的多.
好处如下:
- 可以不通过关键帧动画旋转大于180°.
- 可以用相对值进行旋转而不是绝对值.(设置byValue而不是toValue).
- 可以不创建CATransform3D, 而使用一个简单的数值来指定角度.
- 不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性).
transform.rotation
属性有个奇特的问题是, 它并不存在. 因为CATransform3D并不是一个对象, 它实际上是一个结构体, 也没有符合KVC相关属性, transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性.
你不可以直接设置transform.rotation
或者transform.scale
, 他们不能被直接使用. 当你对他们做动画时候, CoreAnimation自动的根据通过CAValueFunction来计算的值来更新transform属性.
CAValueFunction用于我们赋值给虚拟的transform.rotation
简单浮点值转换成真正的用于摆放图层的CATransfor3D矩阵值. 你可以通过CAPropertyAnimation的valueFunction属性来改变, 于是你设置的函数将会覆盖默认的函数.
CAValueFunction看起来似乎是对那些不能简单相加的属性(例如变换矩阵)做动画的非常有用的机制,但由于CAValueFunction的实现细节是私有的,所以目前不能通过继承它来自定义。你可以通过使用苹果目前已近提供的常量(目前都是和变换矩阵的虚拟属性相关,所以没太多使用场景了,因为这些属性都有了默认的实现方式)。
通过虚拟属性这一小节, 应该弄明白, CAPropertyAnimation的valueFunction的作用是什么.
1.4 动画组
CABaseAnimation的CAKeyframeAnimation仅仅作用于单独的属性, 而CAAnimationGroup可以把这些动画组合在一起. CAAnimationGroup是另一个继承自CAAnimation的子类, 它添加了一个animations数组的属性, 用来组合别的动画.
动画组用起来也是巨简单. 创建对象, 然后将其它动画放到animations数组中即可.
// 创建path动画
CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
keyAnimation.path = bPath.CGPath;
keyAnimation.duration = 3.f;
keyAnimation.rotationMode = kCAAnimationRotateAuto;
// 改变颜色的动画
CAKeyframeAnimation *colorAnimation = [CAKeyframeAnimation animationWithKeyPath:@"backgroundColor"];
colorAnimation.duration = 7;
colorAnimation.values = @[(__bridge id)self.colorView.backgroundColor.CGColor,
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor blackColor].CGColor,
(__bridge id)[UIColor purpleColor].CGColor,
(__bridge id)[UIColor cyanColor].CGColor,
(__bridge id)[UIColor yellowColor].CGColor];
// animationGroup 代码.
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = 7;
animationGroup.animations = @[keyAnimation, colorAnimation];
[plane.layer addAnimation:animationGroup forKey:nil];
2. 过渡动画
对iOS应用程序来说, 属性动画只对图层的可动动画属性起作用, 但是要改版夜歌不能动画属性(比如图片), 或者从层级关系中添加或者移除图层, 属性动画将不起作用.
于是就有了过度动画的概念. 过度动画并不像属性动画那亚航平滑的在两个值之间做做动画, 而是影响到整个图层的变化. 过度动画首先需要展示之前的图层外观, 然后通过一个交换过度到新的外观. 创建过度动画我们使用的是 CATransition
, 同样是另一个CAAnimation子类, 和别的子类不同, CATransform有一个type和subtype来标识变换效果. type是NSString类型值:
kCATransitionFade //淡入淡出
kCATransitionMoveIn //见名知意...
kCATransitionPush
kCATransitionReveal
subtype用来控制动画移动的方向:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
2.1 隐式过度
当设置CALayer的contents属性的时候, CATransition的确是默认的行为. 但是对于与视图关联的图层, 或者是其他隐式动画的欣慰, 这个特性依然是被禁用的, 但是对于自己创建的图层, 这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画.
2.2 对图层树的动画
CATransition并不作用于指定的图层属性, 就是说, 可以在不能准确得知改变什么的情况下对图层做动画, 六在不知道UITableView哪一行被添加或者移除的情况下就可以平滑的刷新它, 或者在不知道UIViewCVontroller内部视图层级的情况下对两个不同的实例做过度动画. 这些例子的变换不涉及到图层的属性, 而是整个图层树的改变, 我们在这种动画的过程中手动的在层级关系中添加或者移除图层.
注意: 要确保CATransition添加到的图层在过度动画发生时候不会再树状结构中被移除, 否则CATransition会和图层一起被移除, 一般来说, 只需要将动画添加到被影响图层的superlayer
.
例如, 对UITabBarController的视图图层做, 切换标签时候淡入淡出效果.
// 首先设置代理, 然后实现代理方法, 在代理中添加过度动画.
// 自定义切换tabbar时候的过度动画
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
transition.duration = 0.2f;
// transition.subtype = kCATransitionFromLeft;
[self.view.layer addAnimation:transition forKey:nil];
}
2.3 自定义动画
过渡动画是一种对那些不太好做平滑动画的属性的强大工具, 但是提供的动画类型太少了.
另外苹果通过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同。UIView过渡方法中options参数可以由如下常量指定:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve
UIViewAnimationOptionTransitionFlipFromTop
UIViewAnimationOptionTransitionFlipFromBottom
但是在iOS平台上还是可以做自定义的过渡动画的. 原理是: 先对目标view进行截图, 将截图放在原来view的上面, 然后对截图进行一些动画操作, 实现自定义过渡动画.
截图: CALayer中有个-renderInContext:
方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。
这里有个警告:-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。
3. 在动画过程中取消动画
之前提到过,你可以用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但并不支持在动画运行过程中修改动画,所以这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。
为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有动画:
- (void)removeAllAnimations;
动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值。一般说来,动画在结束之后被自动移除,除非设置removedOnCompletion为NO,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。