zoukankan      html  css  js  c++  java
  • 定时帧(基于定时器的动画 11.1)

    定时帧

    动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展示像素的时候并不能做到这一点。一般来说这种显示都无法做到连续的移动,能做的仅仅是足够快地展示一系列静态图片,只是看起来像是做了运动。

    我们之前提到过iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。

    在第10章中,我们解决了如何自定义缓冲函数,然后根据需要展示的帧的数组来告诉CAKeyframeAnimation的实例如何去绘制。所有的Core Animation实际上都是按照一定的序列来显示这些帧,那么我们可以自己做到这些么?

    NSTimer

    实际上,我们在第三章“图层几何学”中已经做过类似的东西,就是时钟那个例子,我们用了NSTimer来对钟表的指针做定时动画,一秒钟更新一次,但是如果我们把频率调整成一秒钟更新60次的话,原理是完全相同的。

    我们来试着用NSTimer来修改第十章中弹性球的例子。由于现在我们在定时器启动之后连续计算动画帧,我们需要在类中添加一些额外的属性来存储动画的fromValuetoValueduration和当前的timeOffset(见清单11.1)。

    清单11.1 使用NSTimer实现弹性球动画

      1 @interface ViewController ()
      2 
      3 @property (nonatomic, weak) IBOutlet UIView *containerView;
      4 @property (nonatomic, strong) UIImageView *ballView;
      5 @property (nonatomic, strong) NSTimer *timer;
      6 @property (nonatomic, assign) NSTimeInterval duration;
      7 @property (nonatomic, assign) NSTimeInterval timeOffset;
      8 @property (nonatomic, strong) id fromValue;
      9 @property (nonatomic, strong) id toValue;
     10 
     11 @end
     12 
     13 @implementation ViewController
     14 
     15 - (void)viewDidLoad
     16 {
     17     [super viewDidLoad];
     18     //add ball image view
     19     UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
     20     self.ballView = [[UIImageView alloc] initWithImage:ballImage];
     21     [self.containerView addSubview:self.ballView];
     22     //animate
     23     [self animate];
     24 }
     25 
     26 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
     27 {
     28     //replay animation on tap
     29     [self animate];
     30 }
     31 
     32 float interpolate(float from, float to, float time)
     33 {
     34     return (to - from) * time + from;
     35 }
     36 
     37 - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
     38 {
     39     if ([fromValue isKindOfClass:[NSValue class]]) {
     40         //get type
     41         const char *type = [(NSValue *)fromValue objCType];
     42         if (strcmp(type, @encode(CGPoint)) == 0) {
     43             CGPoint from = [fromValue CGPointValue];
     44             CGPoint to = [toValue CGPointValue];
     45             CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
     46             return [NSValue valueWithCGPoint:result];
     47         }
     48     }
     49     //provide safe default implementation
     50     return (time < 0.5)? fromValue: toValue;
     51 }
     52 
     53 float bounceEaseOut(float t)
     54 {
     55     if (t < 4/11.0) {
     56         return (121 * t * t)/16.0;
     57     } else if (t < 8/11.0) {
     58         return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
     59     } else if (t < 9/10.0) {
     60         return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
     61     }
     62     return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
     63 }
     64 
     65 - (void)animate
     66 {
     67     //reset ball to top of screen
     68     self.ballView.center = CGPointMake(150, 32);
     69     //configure the animation
     70     self.duration = 1.0;
     71     self.timeOffset = 0.0;
     72     self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
     73     self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
     74     //stop the timer if it's already running
     75     [self.timer invalidate];
     76     //start the timer
     77     self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0
     78                                                   target:self
     79                                                 selector:@selector(step:)
     80                                                 userInfo:nil
     81                                                  repeats:YES];
     82 }
     83 
     84 - (void)step:(NSTimer *)step
     85 {
     86     //update time offset
     87     self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration);
     88     //get normalized time offset (in range 0 - 1)
     89     float time = self.timeOffset / self.duration;
     90     //apply easing
     91     time = bounceEaseOut(time);
     92     //interpolate position
     93     id position = [self interpolateFromValue:self.fromValue
     94                                      toValue:self.toValue
     95                                   time:time];
     96     //move ball view to new position
     97     self.ballView.center = [position CGPointValue];
     98     //stop the timer if we've reached the end of the animation
     99     if (self.timeOffset >= self.duration) {
    100         [self.timer invalidate];
    101         self.timer = nil;
    102     }
    103 }
    104 
    105 @end
    View Code

    很赞,而且和基于关键帧例子的代码一样很多,但是如果想一次性在屏幕上对很多东西做动画,很明显就会有很多问题。

    NSTimer并不是最佳方案,为了理解这点,我们需要确切地知道NSTimer是如何工作的。iOS上的每个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:

    • 处理触摸事件
    • 发送和接受网络数据包
    • 执行使用gcd的代码
    • 处理计时器行为
    • 屏幕重绘

    当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。

    屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。

    我们可以通过一些途径来优化:

    • 我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
    • 基于真实帧的持续时间而不是假设的更新频率来做动画。
    • 调整动画计时器的run loop模式,这样就不会被别的事件干扰。

    CADisplayLink

    CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。

    CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

    计算帧的持续时间

    无论是使用NSTimer还是CADisplayLink,我们仍然需要处理一帧的时间超出了预期的六十分之一秒。由于我们不能够计算出一帧真实的持续时间,所以需要手动测量。我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。

    通过比较这些时间,我们就可以得到真实的每帧持续的时间,然后代替硬编码的六十分之一秒。我们来更新一下上个例子(见清单11.2)。

    清单11.2 通过测量没帧持续的时间来使得动画更加平滑

     1 @interface ViewController ()
     2 
     3 @property (nonatomic, weak) IBOutlet UIView *containerView;
     4 @property (nonatomic, strong) UIImageView *ballView;
     5 @property (nonatomic, strong) CADisplayLink *timer;
     6 @property (nonatomic, assign) CFTimeInterval duration;
     7 @property (nonatomic, assign) CFTimeInterval timeOffset;
     8 @property (nonatomic, assign) CFTimeInterval lastStep;
     9 @property (nonatomic, strong) id fromValue;
    10 @property (nonatomic, strong) id toValue;
    11 
    12 @end
    13 
    14 @implementation ViewController
    15 
    16 ...
    17 
    18 - (void)animate
    19 {
    20     //reset ball to top of screen
    21     self.ballView.center = CGPointMake(150, 32);
    22     //configure the animation
    23     self.duration = 1.0;
    24     self.timeOffset = 0.0;
    25     self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    26     self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    27     //stop the timer if it's already running
    28     [self.timer invalidate];
    29     //start the timer
    30     self.lastStep = CACurrentMediaTime();
    31     self.timer = [CADisplayLink displayLinkWithTarget:self
    32                                              selector:@selector(step:)];
    33     [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
    34                      forMode:NSDefaultRunLoopMode];
    35 }
    36 
    37 - (void)step:(CADisplayLink *)timer
    38 {
    39     //calculate time delta
    40     CFTimeInterval thisStep = CACurrentMediaTime();
    41     CFTimeInterval stepDuration = thisStep - self.lastStep;
    42     self.lastStep = thisStep;
    43     //update time offset
    44     self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);
    45     //get normalized time offset (in range 0 - 1)
    46     float time = self.timeOffset / self.duration;
    47     //apply easing
    48     time = bounceEaseOut(time);
    49     //interpolate position
    50     id position = [self interpolateFromValue:self.fromValue toValue:self.toValue
    51                                         time:time];
    52     //move ball view to new position
    53     self.ballView.center = [position CGPointValue];
    54     //stop the timer if we've reached the end of the animation
    55     if (self.timeOffset >= self.duration) {
    56         [self.timer invalidate];
    57         self.timer = nil;
    58     }
    59 }
    60 
    61 @end
    View Code

    Run Loop 模式

    注意到当创建CADisplayLink的时候,我们需要指定一个run looprun loop mode,对于run loop来说,我们就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。

    一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动,一些常见的run loop模式如下:

    1 NSDefaultRunLoopMode - 标准优先级
    2 NSRunLoopCommonModes - 高优先级
    3 UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画

    在我们的例子中,我们是用了NSDefaultRunLoopMode,但是不能保证动画平滑的运行,所以就可以用NSRunLoopCommonModes来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。

    同样可以同时对CADisplayLink指定多个run loop模式,于是我们可以同时加入NSDefaultRunLoopModeUITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:

    1 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
    2 [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    3 [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];

    CADisplayLink类似,NSTimer同样也可以使用不同的run loop模式配置,通过别的函数,而不是+scheduledTimerWithTimeInterval:构造器

    1 self.timer = [NSTimer timerWithTimeInterval:1/60.0
    2                                  target:self
    3                                selector:@selector(step:)
    4                                userInfo:nil
    5                                 repeats:YES];
    6 [[NSRunLoop mainRunLoop] addTimer:self.timer
    7                           forMode:NSRunLoopCommonModes];
  • 相关阅读:
    【Thinking in Java, 4e】初始化与清理
    【Thinking in Java, 4e】控制流程执行
    【Beginning Python】抽象(未完)
    【Python】装饰器 & 偏函数
    【c++ primer, 5e】函数声明 & 分离式编译
    【Python】闭包 & 匿名函数
    【c++ primer, 5e】【函数基础】
    【Python】高阶函数
    变相的取消Datagridview控件的选中状态
    NotifyICon控件使用
  • 原文地址:https://www.cnblogs.com/EchoHG/p/7629012.html
Copyright © 2011-2022 走看看