显式动画

隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础
显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画

属性动画

core animation里面有个很重要的类CAPropertyAnimation(属性动画),它有两个子类,CABasicAnimationCAKeyFrameAnimation

属性动画

CAAnimation在动画结束时会属性会回到原值所以
我们在做了显式动画后可以用CAAnimationDelegate- animationDidStop:finished:方法来更新图层属性(一般设置其为终值),更新属性的时候我们需要一个新的事务并且禁用图层行为.否则会有两次动画.

- (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];
}

但是如果我们有多个动画这个代理回调是无法区分是哪个图层的调用.
1.因为这个代理传进来的动画的深拷贝,所以没有办法通过设置动画属性来分辨.
2.使用-addAnimation:forKey:给动画设置不同key,-animationForKey:找到对应的key来对比,可实现但是比较复杂
3.最简单的方法,CAAnimation实现了KVC,但是更像一个 NSDictionary ,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。

//初始化动画时用KVC设置键值对
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];
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
    //通过kvc获取存储的值
    UIView *handView = [anim valueForKey:@"handView"];
    handView.layer.transform = [anim.toValue CATransform3DValue];
}

CABasicAnimation只能作用于一个属性,只能有初始值和结束值

关键帧动画

CABasicAnimation揭示了大多数隐式动画背后依赖的机制,但是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。

CAKeyframeAnimationCABasicAnimation 类似同样是CAPropertyAnimation 的一个子类它依然作用于单一的一个属性,但是它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。

CAKeyframeAnimation关键帧动画:只我们只需要绘制关键帧的动画,然后Core Animation在每帧之间进行插入。

- (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 ),所以动画会在开始的时候突然由原来色变为蓝色,结束时又突然由结束色变为原来色.

上面是使用animation.values来设置关键帧,我还可以用path 属性指定运动序列.
我们要绘制一个CGPath的三次贝塞尔曲线,可以通过使用一个基于C的Core Graphics绘图指令来创建,不过用UIKit提供的UIBezierPath类会更简单.

UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;
[shipLayer addAnimation:animation forKey:nil];

苹果很人性化的给我们提供了rotationMode属性,设置它为常量 kCAAnimationRotateAuto图层将会根据曲线的切线自动旋转。
animation.rotationMode = kCAAnimationRotateAuto;

虚拟属性

属性动画实际上是针对于关键路径而不是一个键,这就意味着可以对子属性甚至是虚拟属性做动画.

例如:想要对物体做旋转动画,需要作用于transform 属性,因为CALayer没有显式提供角度或者方向之类的属性

CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform";
animation.duration = 2.0;
animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];
[shipLayer addAnimation:animation forKey:nil];
/**
这样确实会旋转180°但是如果我们把M_PI改为2*M_PI就不会旋转了
即使我们设置了byValue也无效,因为变换矩阵并不会像角度值一样叠加
*/

对于CATransform3D因为0度旋转和360度旋转的矩阵完全相同,所以fromValue = toValue因此不会旋转的

我们当然可以用关键帧动画,修改transform属性,实时地重新计算每个时间点的每个变换效果。这是非常复杂的,不推荐

推荐:
我们可以
transform.rotation关键路径应用动画,而不是transform本身

CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];

这样我们就可以简单使用角度而不用复杂的矩阵,就可以使用byValue了. 还可以额外使用transform.position 或者transform.scale

transform.rotation属性其实并不存在,这是因为CATransform3D不是对象而是结构体,它就是一个 CALayer 用于处理动画变换的虚拟属性。当我们transform.rotation等虚拟属性做动画时,Core Animation自动地根据通过 CAValueFunction 来计算的值来更新transform 属性。
CAValueFunction用于把我们赋给虚拟的 transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D 矩阵值。可以通过设置 CAPropertyAnimationvalueFunction属性来改变,于是你设置的函数将会覆盖默认的函数。

动画组

动画组

属性动画仅仅作用于单独的属性,CAAnimationGroup是另一个继承于 CAAnimation 的子类,它添加了一个 animations 数组的属性,用来组合别的动画。

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];

过渡

属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。

因为隐式动画或者属性动画都只能对图层的可动画属性做动画,如果这个属性不可动画,这时候我们就需要过渡了。过渡动画会影响到整个图层的变化,它首先展示的之前的图层外观,然后通过一个交换过渡到新的外观.

我们用CATransition来创建过渡动画,它也是CAAnimation子类,有一个typesubtype来标识变换效果。

//type:
kCATransitionFade//默认,淡入淡出
kCATransitionMoveIn //新图层从顶部滑入
kCATransitionPush //推出老图层
kCATransitionReveal //滑动出原图层,显示新外观,而不是滑动入新图层
//这是系统提供的,我们也可以自定义过渡动画效果

type过渡类型都有一个默认的动画方向(从左侧滑入),可以通过subtype控制方向

//subtype
kCATransitionFromRight
kCATransitionFromLeft//
kCATransitionFromTop
kCATransitionFromBottom

注意
与属性动画类似我们通过-addAnimation:forKey:方法,但是它对指定图层一次只能用一次CATransition,因此,过渡动画都会对它的键设置成“transition”,也就是常量 kCATransition 。

隐式过渡

过渡动画和属性动画类似也是有隐式,如果设置了CALayercontents属性,但是对于与视图关联的图层隐式过渡动画是被禁用的,但是如果是自己创建的图层contents图片做的改动都会自动附上淡入淡出的动画。

注意
虽然我们用过渡动画来对图层的某些不可动画属性做动画,但是也不是说过渡动画不能对可动画属性做动画.

    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromRight;
    [self.containerView.layer addAnimation:transition forKey:nil];
    self.containerView.backgroundColor = [UIColor redColor];

图层树动画

添加过渡动画不需要指定属性,它涉及到整个图层树的改变,因此我们可以在即使不能准确得知图层改变什么的情况下对图层做动画.

确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer

自定义动画

UIView也有提供过渡动画函数:
UIView +transitionFromView:toView:duration:options:completion:+transiti
onWithView:duration:options:animations:

这些过渡方法中options参数可以由如下常量指定:

UIViewAnimationOptionTransitionFlipFromLeft//以Y轴从左向右旋转
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp//从上向下翻页效果
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve//溶解渐变效果
UIViewAnimationOptionTransitionFlipFromTop//以X轴从下向上旋转
UIViewAnimationOptionTransitionFlipFromBottom

因此只需要根据要实现效果CATransition还是UIView的过渡方法就行了.

如果真的要实现自定义的过渡效果:
因为过渡动画的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们会对图层截图,就可以用属性动画来代替过渡动画了.

对图层截图的方法:CALayer有一个 - renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。

- (IBAction)performTransition
{
    //preserve the current view snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
    //insert snapshot view in front of this one
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];
    //update the view (we'll simply randomize the layer background color)
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //perform animation (anything you like)
    [UIView animateWithDuration:1.0 animations:^{
        //scale, rotate and fade the view
        CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
        transform = CGAffineTransformRotate(transform, M_PI_2);
        coverView.transform = transform;
        coverView.alpha = 0.0;
    } completion:^(BOOL finished) {
        //remove the cover view now we're finished with it
        [coverView removeFromSuperview];
    }];
}

注意
- renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。

动画过程中取消动画

添加动画:-addAnimation:forKey:
检索动画:- (CAAnimation *)animationForKey:(NSString *)key
不支持动画过程中修改动画
移除指定动画:- (void)removeAnimationForKey:(NSString *)key;
移除所有动画:- (void)removeAllAnimations;

注意
一般说来,动画在结束之后被自动移除,除非设置removedOnCompletionNO,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。