2012年12月8日 星期六

Core Animation的基本使用(九)

CALayer的时间系统



參考網頁: http://blog.csdn.net/leonpengweicn/article/details/8265290



CALayer通过CAMediaTiming协议实现了一个有层级关系的时间系统.除了CALayer,CAAnimation也采纳了此协议,用来实现动画的时间系统.

在 CA中,有一个Absolute Time(绝对时间)的概念,可以通过CACurrentMediaTime()获得,其实这个绝对时间就是将mach_absolute_time() 转换成秒后的值.这个时间和系统的uptime有关,系统重启后CACurrentMediaTime()会被重置.
就和座标存在相对座标一样,不同的实现了CAMediaTiming协议的存在层级关系的对象也存在相对时间,经常需要进行时间的转换,CALayer提供了两个时间转换的方法:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;  
现在来重点研究CAMediaTiming协议中几个重要的属性.

beginTime

无论是图层还是动画,都有一个时间线Timeline的概念,他们的beginTime是相对于父级对象的开始时间. 虽然苹果的文档中没有指明,但是通过代码测试可以发现,默认情况下所有的CALayer图层的时间线都是一致的,他们的beginTime都是0,绝对时 间转换到当前Layer中的时间大小就是绝对时间的大小.所以对于图层而言,虽然创建有先后,但是他们的时间线都是一致的(只要不主动去修改某个图层的 beginTime),所以我们可以想象成所有的图层默认都是从系统重启后开始了他们的时间线的计时.
但是动画的时间线的情况就不同了,当一个动画创建好,被加入到某个Layer的时候,会先被拷贝一份出来用于加入当前的图层,在CA事务被提交的时候,如 果图层中的动画的beginTime为0,则beginTime会被设定为当前图层的当前时间,使得动画立即开始.如果你想某个直接加入图层的动画稍后执 行,可以通过手动设置这个动画的beginTime,但需要注意的是这个beginTime需要为 CACurrentMediaTime()+延迟的秒数,因为beginTime是指其父级对象的时间线上的某个时间,这个时候动画的父级对象为加入的这 个图层,图层当前的时间其实为[layer convertTime:CACurrentMediaTime() fromLayer:nil],其实就等于CACurrentMediaTime(),那么再在这个layer的时间线上往后延迟一定的秒数便得到上面的 那个结果.

timeOffset

这个timeOffset可能是这几个属性中比较难理解的一个,官方的文档也没有讲的很清楚. local time也分成两种一种是active local time 一种是basic local time.
timeOffset则是active local time的偏移量.
你将一个动画看作一个环,timeOffset改变的其实是动画在环内的起点,比如一个duration为5秒的动画,将timeOffset设置为2(或者7,模5为2),那么动画的运行则是从原来的2秒开始到5秒,接着再0秒到2秒,完成一次动画.

speed

speed属性用于设置当前对象的时间流相对于父级对象时间流的流逝速度,比如一个动画beginTime是0,但是speed是2,那么这个动画的1秒 处相当于父级对象时间流中的2秒处. speed越大则说明时间流逝速度越快,那动画也就越快.比如一个speed为2的layer其所有的父辈的speed都是1,它有一个 subLayer,speed也为2,那么一个8秒的动画在这个运行于这个subLayer只需2秒(8 / (2 * 2)).所以speed有叠加的效果.

fillMode

fillMode的作用就是决定当前对象过了非active时间段的行为. 比如动画开始之前,动画结束之后。如果是一个动画CAAnimation,则需要将其removedOnCompletion设置为NO,要不然 fillMode不起作用. 下面来讲各个fillMode的意义
kCAFillModeRemoved 这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态
kCAFillModeBackwards 这 个和kCAFillModeForwards是相对的,就是在动画开始前,你只要将动画加入了一个layer,layer便立即进入动画的初始状态并等待 动画开始.你可以这样设定测试代码,将一个动画加入一个layer的时候延迟5秒执行.然后就会发现在动画没有开始的时候,只要动画被加入了 layer,layer便处于动画初始状态
kCAFillModeBoth 理解了上面两个,这个就很好理解了,这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态.
其他的一些参数都是比较容易理解的.

实际应用

参见苹果官方 QA1673 How to pause the animation of a layer tree
-(void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}
 

以下實驗暫停及啓動的實作,本實驗一併加上時間的顯示。


1.延續前一個專案,在storyboard再加上一個Label及一個Button
2. 在mainViewController.h加上計算時間所需的變數。
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface mainViewController : UIViewController

@property (weak)   CALayer *layer;
@property (weak)   CALayer *layer2;

@property (weak, nonatomic) IBOutlet UISwitch *actionsSwitch;

@property (nonatomic) BOOL pauseState;
@property (nonatomic) double currentTimeValue;

@property (nonatomic) NSTimer  *timer;

@property (weak, nonatomic) IBOutlet UILabel *timeShowLabel;

@property (weak, nonatomic) IBOutlet UIButton *pauseButton;

@end

3. 在mainViewController.m加上程式碼,此時間的處理只加在Scale這個變化上,其它的用法是一樣的,就不再做了。
@synthesize pauseState;
@synthesize timeShowLabel;
@synthesize timer;
@synthesize currentTimeValue;
@synthesize pauseButton;

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
   
    layer = [CALayer layer];
    layer.bounds = CGRectMake(0, 0, 200, 200);
    layer.position =  ORGINAL_POSITION;   ///CGPointMake(500, 300);
    layer.backgroundColor = [UIColor redColor].CGColor;
    layer.borderColor = [UIColor blackColor].CGColor;
    layer.opacity = 1.0f;
    [self.view.layer addSublayer:layer];
   
    actionsSwitch.on = NO;
   
    UIImage *image = [UIImage imageNamed:@"TaiwanMap.jpg"];
    layer2 = [CALayer layer];
    layer2.contents = (id)image.CGImage;
    layer2.bounds = CGRectMake(0, 0, 200, 200);
    layer2.position = CGPointMake(350, 650);
    [self.view.layer addSublayer:layer2];
   
    pauseState = NO;
}

- (IBAction)scale:(id)sender {
   
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    animation.toValue = [NSNumber numberWithFloat:2.0];
    animation.fromValue = [NSNumber numberWithFloat:1.0];
    animation.duration = 3.0;
    animation.repeatCount = 3;
    animation.removedOnCompletion=NO;
    animation.autoreverses=YES;
    animation.fillMode=kCAFillModeForwards;
   
    [layer2 addAnimation:animation forKey:@"scale"];
   
    timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timeLoader) userInfo:nil repeats:YES];
   
    currentTimeValue = [layer2 convertTime:CACurrentMediaTime() fromLayer:nil];

 

}

-(void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer2 convertTime:CACurrentMediaTime() fromLayer:nil];
    layer2.speed = 0.0;
    layer2.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer2 timeOffset];
    layer2.speed = 1.0;
    layer2.timeOffset = 0.0;
    layer2.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer2 convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer2.beginTime = timeSincePause;

   
}
- (IBAction)pause:(id)sender {
  
    pauseState = ~pauseState;
  
    if (pauseState != NO){
        [pauseButton setTitle:@"啟動" forState:UIControlStateNormal];
        [self pauseLayer:layer];
    }
    else {
        [pauseButton setTitle:@"暫停" forState:UIControlStateNormal];
        [self resumeLayer:layer];
    }
}

-(void)timeLoader
{

    [self showTime];
}


- (void) showTime
{
    NSTimeInterval  time =  [layer2 convertTime:CACurrentMediaTime() fromLayer:nil];
  
    time = time - currentTimeValue;
  
    if (time >(3*3*2)){  // duration * repeat *autoreverse
       
        [timer invalidate];
      
        timer = nil;
      
        [pauseButton setTitle:@"暫停" forState:UIControlStateNormal];
    }
  
    NSInteger min;
    NSInteger sec;
    NSInteger hour;
  
    float minsec;
  
    min = (NSInteger ) time/60;
  
    sec = (NSInteger ) time%60;
  
    hour = (NSInteger) time/(60*24);
  
    minsec = time - sec - min*60;
  
    NSString *showTimeText =  [NSString stringWithFormat:@"Sec = %2.2f", sec+minsec];
  
    timeShowLabel.text = showTimeText;
  
}


4. 結果,在動作中可以顯示時間,然後暫停後再次啓動。




沒有留言: