iOS Core Animation - Setting Things in Motion

前言

iOS Core Animation 读书笔记(二) Setting Things in Motion

笔记

Implicit Animations

隐式的动画由framework自动执行,除非你叫他不执行。

Transcations

  1. Core Animation会去假设你所有创建在屏幕上的东西都会animation。所以animation并不用你刻意的去启动。这也是Core Animation比OpenGL慢的一点,OpenGL不用去考虑这些东西,就不需要花费额外的性能代价。

  2. 当你改变一个CALayer的属性时,它会自动平滑的执行动画从当前属性到新的属性,而不是瞬变。不需要你额外的做些什么。这种动画就叫做隐式动画。

  3. Transcations是Core Animation将一系列动画汇集成一个的机制。一旦transaction提交(committed)之后,这些动画就会开始执行。

  4. CATransaction类控制着transaction的行为。该类没有alloc init这样的初始化,也并不是如名字那样代表一个transaction,它表示一个transaction的栈。可以使用+begin-commit方法来将一个transaction进栈或者出栈。

  5. 使用+setAnimationDuration:方法来设置当前transaction的动画时间。没有指定的话,默认是0.25s。使用该方法时,最好显示的开始一个新的transaction,将其压入栈中,表示只改变这个transaction的duration;不然可能改变同时发生的一些animation,比如旋转屏幕。

  6. 看一下代码:

    - (IBAction)changeColor {
        //begin a new transaction
        [CATransaction begin];
    
        //set the animation duration to 1 second
        [CATransaction setAnimationDuration:1.0];
    
        //randomize the layer background color
        CGFloat red = arc4random() / (CGFloat)INT_MAX;
        CGFloat green = arc4random() / (CGFloat)INT_MAX;
        CGFloat blue = arc4random() / (CGFloat)INT_MAX; 
        self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    
         //commit the transaction
        [CATransaction commit]; 
    }
    

    这里使用+begin方法,表示显式的开始一个transaction,transaction其实就是一个属性改变的动画的集合;显式开始一个transaction可以精准的控制这一系列动画的执行时间等等,而且不予其他的动画搞混。当执行+commit方法时,这些动画变开始执行。而对于一些没有使用begin,commit的动画,其实他们也是属于不同的transaction,只是这些transaction没有被显式的压入(begin)或推出(commit)transaction的数组而已;他们被隐式的执行罢了。

  7. 其实UIView的+animateWithDuration:animations:方法,也是在内部调用上述6中的方法而已。

  8. UIView的+beginAnimations:context:+commitAnimations和CALayer的+begin以及+commit相同。

Completion Blocks

  1. UIView中的animation的completion block实际上就是调用CATransaction的+setCompletionBlock:方法。

Layer Actions

CALayer在属性变化时执行的动画叫做actions,来看看整个的流程。

  1. 当CALayer的一个属性变化时,调用-actionsForKey:方法,这里传入的key就是这个属性的名字。
  2. 这时layer先查看他是否有delegate,也就是CALayerDelegate;再看看这个delegate里面是否实现了-actionForLayer:forKey:方法。如果实现了这个方法,那么就会调用这个方法并且返回。
  3. 如果没有这个delegate或者delegate里面没有实现上述的方法,那么layer就会去检查他的actions属性。这个字典包含了属性名到action的映射。
  4. 如果还是没找到这个要动画的属性名的话,就去style字典里面找。
  5. 最后的最后,还是没有找到的话,就会调用-defaultActionForKey:方法;这个方法定义了标准的动画为那些已知的属性。

如果上述步骤的第二步中的-actionForLayer:forKey:方法返回nil的话就不会有动画发生了;如果返回的是一个符合CAAction协议的对象,比如返回CABasicAnimation,那么就会执行动画。

因为UIView的backing layer的delegate是UIView本身。UIView实现-actionForLayer:forKey:方法的方式就是:如果UIView的属性改变不发生在一个显式的动画工程之中就返回nil;如果在动画过程之中,就返回可动画的对象。测试代码如下:

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView; 
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);

    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations]; 
}
@end

打印的结果如下:

$ LayerTest[21215:c07] Outside: <null>
$ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

所以说UIView是disable隐式动画的。

[CATransaction setDisableActions:YES];这个方法也可以 禁用动画。

在layer的actions字典属性里面设置如下:

CATransition *transition = [CATransition animation]; 
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft; 
self.colorLayer.actions = @{@"backgroundColor": transition};

Presentation Versus Model

  1. 当你更改一个CALayer的属性时,这个属性是立刻发生改变的,但是并不在屏幕上立即显示出来。
  2. 当改变CALayer的属性时,CALayer相当于一个model,用来存储动画结束时该呈现何种状态;而Core Animation则相当于controller,用来控制view的变化。这也是一个MVC结构啊。所以有时候layer tree也叫作model layer tree。
  3. iOS中,屏幕1/60秒重新绘制一次。所以如果动画时间超过这一时长的话,那么Core Animation就需要从设置动画开始到动画结束内每1/60秒更新一次界面。所以这也需要CALayer能够记住当前的属性值和最终的属性值。
  4. 动画过程中,当前显示的layer的属性值,存储在一个presentation layer中,它可以通过-presentationLayer;方法取得。这个layer实际上就是model layer的复制,唯一的区别就是当前动画的属性值不同。当在presentation layer上调用-modelLayer方法时,返回的是presentation layer的原始的model layer。

Explicit Animations

使用显式动画的好处是可以完全控制动画的行为,比如运动轨迹,动画时间等等。

Property Animations

  1. CAAnimation类是所有Core Animation支持的动画类的共有的父类;它有一个子类是CAPropertyAnimation,也就是属性动画。

  2. CABasicAnimation是CAPropertyAnimation的一个子类。CAPropertyAnimation指定一个属性通过指定keypath。CABasicAnimation有3个属性:fromValue, toValue, byValue。这三个都是id类型,因为他们可以是数值,颜色,图片等等。当然只需要指定3个中的两个就可以确定一个动画。

  3. 使用CABasicAnimation就是显式动画了,来看一个例子:

    //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];
    
  4. 使用上述的代码在动画结束后,颜色还是会回到以前的颜色,是因为动画只是改变了presentation layer,并没有改变model layer。实际上,隐式动画改变属性时,动画使用的也是CABasicAnimation,属性改变时,会在-actionForLayer:forKey:方法中返回一个CABasicAnimation。所以我们可以在动画执行之前改变属性,如下:

    animation.fromValue = (__bridge id)self.colorLayer.backgroundColor; self.colorLayer.backgroundColor = color.CGColor;
    
  1. 选择在动画之前改变属性值,而不是在动画结束之后。是因为,在动画结束之后,会先跳回原始值,在跳到最终值。

  2. 使用CAAnimationDelegate可以解决上述问题。它可以知道动画准确的结束时间。实现-animationDidStop:finished:方法即可。代码如下:

    // first specify delegate
    animation.delegate = self;
    
    // implement delegate method
    - (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];
    }
    
  3. CABasicAnimation在使用-addAnimation:forKey:方法添加给CALayer时,可以指定一个NSString类型的Key。这个Key可以唯一的标示一个animation。所以在-animationDidStop:finished:方法中可以使用key来辨别是否是某个特定的animation。

  4. CAAnimation还有一套自己的方法,来标示一个animation。可以使用-setValue:forKey:以及-valueForKey:方法。代码如下:

    // when set up animation
    [animation setValue:handView forKey:@"handView"];
    
    // when finish animation
    - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
          // get value that set last step
          UIView *handView = [anim valueForKey:@"handView"];
          // do something later
    }
    
  5. 注意:这个动画结束的delegate被调用,可能发生在其值回退之前。所以,为什么要说在动画之前就设置动画结束的值。

  6. CAKeyframeAnimation很强大,和CABasicAnimation一样,也是CAPropertyAnimation的一个子类。它和CABasicAnimation一样,也只对一个属性进行动画。CAKeyframeAnimation只需要你来提供关键帧,然后Core 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];

通过values属性(是一个NSArray),指定关键帧。必须指定开始和结束的帧,因为CAKeyframeAnimation不会默认使用这个属性当前的值来作为第一帧。

  1. CAKeyframeAnimation还可以使用CGPath。来看看例子:
   //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];

指定CAKeyframeAnimation的path属性为一个CGPath。然后就可以沿着这个path运动。另外,CAKeyframeAnimation还有一个rotationMode属性,该属性指定动画的layer的旋转特性。将该属性指定为kCAAnimationRotationAuto的话,上面的shipLayer就会一直旋转,以调整角度去对齐bezierPath的切线方向。

  1. CAPropertyAnimation不单单可以作用于实际存在的属性,还可以作用于虚拟的属性。是一个keys path:比如animation.keyPath = @"transform.rotation";

Animation Groups

  1. CAAnimationGroup是CAAnimation的一个子类,他可以聚集一堆动画。
  2. 他有一个animations属性,是一个包含CAAnimation的数组。

Transitions

CATransition也是CAAnimation的一个子类,它影响整个layer。它会先对原先的layer照个快照,然后创建新的layer。之后以动画的形式,将新的layer快照,替代原先的layer快照,比方说从左向右推入。它主要用来动画一些不可动画的属性,比如UIImage的image属性。看看代码:

@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

CALayer的contents属性,自动应用CATransition动画,当改变一个不是backing layer的contents时,会自动应用动画。

自己实现CATransaction效果:

  1. CALayer的-renderInContext:方法可以用来获取将当前的layer的contents画到一个context里面,也就实现了获取当前的快照。
  2. 将这个快照放在当前屏幕的最上面,掩盖住原来的view;之后就可以在原来的view上面进行变化了。
  3. 再使用动画推出这个快照,就完成了transition。

看看代码实现:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    UIView *baseView = [[UIView alloc]initWithFrame:self.view.bounds];
    baseView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:baseView];

    // preserve the current snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [baseView.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();

    // insert snapshot view
    UIImageView *coverView = [[UIImageView alloc]initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];

    // update original view
    baseView.backgroundColor = [UIColor redColor];
    [UIView animateWithDuration:1.0 animations:^{
        CGAffineTransform transform = CGAffineTransformMakeScale(0.1, 0.1);
        transform = CGAffineTransformRotate(transform, M_PI_2);
        coverView.transform = transform;
        coverView.alpha = 0.5;
    } completion:^(BOOL finished) {
        [coverView removeFromSuperview];
    }];
}

Canceling an Animation in Progress

  1. 当使用CALayer的-addAnimation:forKey:方法给一个layer添加了动画之后,可以使用-(CAAnimation *)animationForKey:(NSString *)key;方法来根据key重新获取这个animation。
  2. 修改正在运行的动画的属性,是没有效果的。
  3. 可以使用- (void)removeAnimationForKey:(NSString *)key;方法来删除layer的某个animation。
  4. 可以使用- (void)removeAllAnimations;来删除layer的所有动画。
  5. 只要一删除动画,屏幕就会根据model layer来重新绘制。

Layer Time

The CAMediaTiming Protocol

  1. CAMediaTiming协议定义了多种属性及方法用来控制动画工程中的时间,CALayer和CAAnimation都符合该协议。

  2. duration属性:一次动画执行的时间。

  3. repeatCount属性:动画重复执行的次数。可以是小数,比如3.5次。。

  4. duration和repeatCount的默认值都是0,但不表示0次和0秒。表示的是默认的0.25s和1次。

  5. 还可以使用repeatDuration属性,指定整个重复动画的时间;autoreverses属性指定动画是否要原路径反着来一遍。

  6. 每个动画的时间都是各成体系的。每个动画内的时间可以被延迟,加速,减速等等。

  7. beginTime属性:如果一个animation是在一个animation group中,则beginTime就是其parent object——animation group 开始的一个偏移。如果一个animation 的 beginTime为5,则此动画在group aniamtion开始之后的5s在开始动画。如果一个animation是直接添加在layer上,beginTime同样是是其parent object——layer 开始的一个偏移,但是一个layer的beginning是一个过去的时间(猜想layer的beginning可能是其被添加到layer tree上的时间),因此不能简单的设置beginTime为5去延迟动画5s之后开始,因为有可能layer的beginning加上5s之后也是一个过去的时间(很有可能),因此,当要延迟一个添加到layer上的动画的时候,需要定义一个addTime,因此:

    addTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; // 取得layer时间系下的现在时间
    animation.beginTime = addTime + delay; // 设置animation的时间系的原点,要参照layer的时间系。
    

    如果一个layer他自己的beginTime已经设置,则animation的addTime的计算必须在layer的beginTime设置之后,因为要有一个时间的转移,具体看下面的例子:

    CFTimeInterval currentTime = CACurrentMediaTime();
    CFTimeInterval currentTimeInSuperLayer = [superLayer convertTime:currentTime fromLayer:nil];
    layer.beginTime = currentTimeInSuperLayer + 2; // 设置layer的时间系 从layer的superLayer进行设置。也就是说layer的时间系的原点是现在的Global时间在superLayer时间系里面加2s。 但是要把现在的global时间换成superLayer的时间。
    CFTimeInterval currentTimeInLayer = [layer convertTime:currentTimeInSuperLayer fromLayer:superLayer];
    CFTimeInterval addTime = currentTimeInLayer;
    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.beginTime = addTime + 1; // 设置groupAnimation的时间系, 是现在时间的Global时间在layer的时间系里面加1s。(若speed都一样的话,那么就是layer时间系的-1s的位置)。
    group.animations = [NSArray arrayWithObject:anim];
    group.duration = 2;
    anim.beginTime = 0.5;
    [layer addAnimation:group forKey:nil];
    

  8. speed属性:表示的是时间的系数。默认为1,表示1倍速。如果duration是1s,但是speed是2,那么执行时间只有0.5s,也就是2倍速。

  9. timeOffset属性:表示动画从timeOffset的位置开始,动画到终点结束后回到起点再动画到timeOffset的位置。它计算出来的开始位置不受speed的影响。

  10. 当设置removeOnCompletion属性为NO时,说明动画完成时,这个动画没有从layer上面删除。这时候可以设置fillMode属性。这个属性默认为kCAFillModeRemoved,表示被动画的属性在model layer上面都不曾变化。可以将fillMode属性设置成其他的,那么model layer上面的属性会在动画结束时自动更新将其设置成presentation layer上面一样的值。这个时候要注意,在添加animation的时候要指定一个key,以确保之后可以方便删除这个动画。

Hierarchical Time

  1. layer的动画时间也是有相对坐标系的,类似layer的位置,时间也是相对于super layer的。

  2. 改变CALayer或者CAGroupAnimation的beginTime,timeOffset以及speed属性会影响到sublayer的动画时间。

  3. Core Animation有一个global time的概念,也就是一个设备的各个程序间共同的一个时间体系。它是一个mach time,可以通过下列代码获得当前的global time:

    CFTimeInterval time = CACurrentMediaTime();
    

    这个函数返回的值很奇怪,它表示设备从上一次reboot到现在的秒数。

  4. 每个CALayer和CAAnimation还有自己的一个local time的概念。可以将2个layer之间的时间进行转换:

    - (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 
    - (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
    
  5. 使用self.window.layer.speed = 100;会使你的整个app以100倍速运行。

  6. 来看一个自己写的代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        UIView *baseView1 = [[UIView alloc]initWithFrame:CGRectMake(50, 50, 50, 50)];
        baseView1.backgroundColor = [UIColor redColor];
        [self.view addSubview:baseView1];
        UIView *baseView2 = [[UIView alloc]initWithFrame:CGRectMake(50, 250, 50, 50)];
        baseView2.backgroundColor = [UIColor redColor];
        [self.view addSubview:baseView2];
        UIView *baseView3 = [[UIView alloc]initWithFrame:CGRectMake(50, 450, 50, 50)];
        baseView3.backgroundColor = [UIColor redColor];
        [self.view addSubview:baseView3];
        UIView *baseView = [[UIView alloc]initWithFrame:CGRectMake(50, 50, 50, 50)];
        baseView.backgroundColor = [UIColor yellowColor];
        [self.view addSubview:baseView];
    
        CABasicAnimation *animation = [CABasicAnimation animation];
        animation.duration = 4;
        //animation.repeatCount = 2;
        //baseView.layer.speed = 0.5;
        animation.keyPath = @"position";
        animation.toValue = [NSValue valueWithCGPoint:CGPointMake(75, 475)];
        animation.timeOffset = 2;
        //animation.removedOnCompletion = NO;
        //animation.fillMode = kCAFillModeBoth;
        animation.beginTime = [baseView.layer convertTime:CACurrentMediaTime() fromLayer:nil] + 2;
        [baseView.layer addAnimation:animation forKey:@"base"];
    }
    

Manual Animation

看段代码:

@interface ViewController ()
@property (nonatomic, strong) CALayer *doorLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.doorLayer = [CALayer layer];
    self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
    self.doorLayer.position = CGPointMake(150 - 64, 150);
    self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
    //self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
    self.doorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.doorLayer];

    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0;
    self.view.layer.sublayerTransform = perspective;
    //add pan gesture recognizer to handle swipes
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
    [pan addTarget:self action:@selector(pan:)];
    [self.view addGestureRecognizer:pan];
    //pause all layer animations
    self.doorLayer.speed = 0.0;
    //apply swinging animation (which won't play because layer is paused)
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y"; animation.toValue = @(-M_PI_2);
    animation.duration = 1.0;
    [self.doorLayer addAnimation:animation forKey:nil];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)pan:(UIPanGestureRecognizer *)pan {
    //get horizontal component of pan gesture
    CGFloat x = [pan translationInView:self.view].x;
    //convert from points to animation duration //using a reasonable scale factor
    x /= 200.0f;
    //update timeOffset and clamp result
    CFTimeInterval timeOffset = self.doorLayer.timeOffset;
    timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
    self.doorLayer.timeOffset = timeOffset;
    //reset pan gesture
    [pan setTranslation:CGPointZero inView:self.view];
}

@end

由于在动画执行过程中无法改变animation的属性,所以可以更改它的上一级,也就是layer的属性。将speed设置成0,所以动画暂停,相当于动画一直在执行却始终是在第一帧上面执行。根据手势来改变layer的timeOffset,那么在下一次动画渲染屏幕的时候(也就是画下一帧presentation layer的时候),动画发现还处在第一帧,但是timeOffset已经变了,所以就跟着手指移动了。

Easing

Animation Velocity

  1. Easing是为了模仿现实中的有加速度的这种运动。
  2. 为了使用easing,可以设置CAAnimation的timingFunction属性;也可以使用CATransaction的+setAnimationTimingFunction:方法将easing应用到隐式动画上面。这个属性是一个CAMediaTimingFunction对象。
  3. 创建CAMediaTimingFunction对象的方法很多。简单的就是使用+timingFunctionWithName:方法。在UIView的动画中,kCAMediaTimingFunctionEaseInEaseOut是默认的,但是CAAnimation就要你自己去指定一个。注意隐式动画中的默认值是kCAMediaTimingFunctionDefault,但是显式动画中没有默认的值。
  4. CAKeyframeAnimation有一个timingFunctions属性,它是一个数组。数组的长度是keyframes数组的长度减一。它表示每个关键帧之间的easing属性。

Custom Easing Functions

  1. 可以使用CAMediaTimingFunction的+functionWithControlPoints::::方法来创建自定义的easing效果。这个方法的命名不太符合apple的规范。

  2. 上面函数的4个点用来构成一个bezierPath,这个bezierPath可以表示这个动画的easing效果。bezierPath的切线就是速度。todo 图片

  3. apple定义的几个easing效果也是这样实现的,可以使用CAMediaTimingFunction的-getControlPointAtIndex:values:方法来获取4个点。第一个点是起点,第四个点是终点,中间两个点是控制点1和控制点2。来看看将几种系统实现的easing效果的bezierPath画出来的代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        //create timing function
        CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        //get control points
        CGPoint controlPoint1, controlPoint2;
        [function getControlPointAtIndex:1 values:(float *)&controlPoint1];         [function getControlPointAtIndex:2 values:(float *)&controlPoint2];
        //create curve
        UIBezierPath *path = [[UIBezierPath alloc] init]; 
          [path moveToPoint:CGPointZero];
        [path addCurveToPoint:CGPointMake(1, 1) controlPoint1:controlPoint1 controlPoint2:controlPoint2];
        //scale the path up to a reasonable size for display
        [path applyTransform:CGAffineTransformMakeScale(200, 200)];
        //create shape layer
        CAShapeLayer *shapeLayer = [CAShapeLayer layer]; 
          shapeLayer.strokeColor = [UIColor redColor].CGColor; 
          shapeLayer.fillColor = [UIColor clearColor].CGColor; 
          shapeLayer.lineWidth = 4.0f;
        shapeLayer.path = path.CGPath; 
          [self.layerView.layer addSublayer:shapeLayer];
        //flip geometry so that 0,0 is in the bottom-left
        self.layerView.layer.geometryFlipped = YES; 
    }
    
  4. 奇怪的是这个函数的调用,它不传CGPoint,而是传递的float。如下:

    [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
    

    其中(1,0)表示第一个控制点,(0.75, 1)表示第二个控制点。

  5. 至于更加复杂的easing效果可以使用Keyframe-Based Easing。就是将复杂的easing分成多个keyframe,每个keyframe之间由一个简单的自定义easing效果来实现。这个方法需要繁琐的计算量。 todo 复杂的bezierPath的图片

  6. 还有一个简单的方法就是:每1/60s定义一个keyframe,在每个keyframe之间都是线性变化的。只要求得每个keyframe的位置就可以做出流畅的动画。取1/60s是因为iOS1/60s渲染一次屏幕,这个值已经是最佳的了,每次渲染屏幕的时候都有一个自己设的值,已经是到达最精确的控制了。其实这个时候两个关键帧之间用什么变化都无所谓的,他只是渲染两次而已,不存在什么变化。至于每个关键帧的位置如何求得,可以在http://robertpenner.com/easing找到算法。

Timer-Based Animation

Frame Timing

  1. 上面讲到的1/60s定义一个keyframe,其实还可以使用NSTimer来实现。也就是1/60s就执行一次更新属性的操作,然后就会重新绘制出来。但是使用NSTimer会不太精确。

  2. NSTimer和屏幕重新绘制都是主线程的任务,主线程中的任务都要等到之前的任务执行完成后再开始执行。所以当到了执行NSTimer或者当需要开始重新绘制屏幕时,如果这时候之前的任务还没有执行结束,则需要等待。这时无论NSTimer还是重新绘制都会有一些延迟。所以可能出现,需要绘制时,NSTimer还没更新,或者在一次绘制时NSTimer已经更新了多次的情况。这时候就会出现animation的迟滞或者跳帧。看看这个的解决方案。

  3. CADisplayLink和NSTimer类似,但是他能保证在屏幕重新绘制之前被触发。但是他不是通过指定一个timeInterval来触发,而是通过frameInterval来指定两次触发之间相隔多少帧。但是它还是无法保证重新绘制屏幕能按时发生。

  4. 根据上面的问题,这时就需要精确计算上一帧发生的时间,和当前帧发生的时间。两者相减就可以知道间隔时间,就能算出现在这一帧该动画到什么程度。来看看代码:

    - (void)animate {
        //reset ball to top of screen
        self.ballView.center = CGPointMake(150, 32);
        //configure the animation
        self.duration = 1.0;
        self.timeOffset = 0.0;
        self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];             self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
        //stop the timer if it's already running
        [self.timer invalidate];
        //start the timer
        self.lastStep = CACurrentMediaTime();
        self.timer = [CADisplayLink displayLinkWithTarget:self
        selector:@selector(step:)]; 
          [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    }
    - (void)step:(CADisplayLink *)timer {
        //calculate time delta
        CFTimeInterval thisStep = CACurrentMediaTime(); 
          CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep;
        //update time offset
        self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);
        //get normalized time offset (in range 0 - 1)
        float time = self.timeOffset / self.duration;
        //apply easing
        time = bounceEaseOut(time);
        //interpolate position
        id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; 
          self.ballView.center = [position CGPointValue];
        //stop the timer if we've reached the end of the animation
        if (self.timeOffset >= self.duration) {
            [self.timer invalidate];
            self.timer = nil; 
        }
    }
    

Physical Simulation

看的晕头转向。。。

参考

  1. CAMediaTiming 协议属性详解
  2. Robert Penner`s Easing Functions

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器