第九篇:基于定时器的动画
目录
一、定时器动画不用NSTimer的理由
二、使用CADisplayLink来做定时器动画
三、计算每帧的持续时间
一、定时器动画不用NSTimer的理由
-
我们知道iOS刷新屏幕的频率是60Hz,即每秒钟刷新屏幕60次。通常情况下,如果有个基于定时器的动画,我们一般会采用NSTimer来实现,能实现效果,看起来也没什么问题,但其实对于这种基于定时器的动画,使用NSTimer并不是最佳选择。
-
因为在前面学习runLoop的时候,我们提到过timer必须得添加到指定的runLoop模式下才能起作用,否则timer是不起作用的,而runLoop为了节省资源也并不会在非常准确的时间点调用定时器。比方说现在有一个timer的时间间隔为1s,runLoop在第一次循环中执行完timer的回调后立马进入了第二个循环,但是在第二个循环中需要做一个很大计算量的任务,计算时间超过了1s,那么timer的第二个回调就会被错过,这次回调错过了就错过了,不会说是把这次回调延后执行,而是会等到下个时间点直接执行timer的第三次回调。好,先不说计算量大的这种延时,一个runLoop通常需要处理一些手势事件、响应一些系统事件、处理定时器事件、UI的绘制与更新等,这些任务都是按着顺序执行的,也就是说timer的触发必须得等到它的上个任务完成了才会执行,因此也不能在我们设定的那个十分准确的时间点来执行事件,通常会有几毫秒的延迟,这些延时也都是一些随机值,这样即便我们设置timer每秒钟执行60次,和屏幕刷新的频率一样,也难以确保它就真得每秒钟执行60次,所以就有可能timer的触发是在屏幕刷新之后一点点,这样动画就会有个延时,看起来像是屏幕卡顿了,也有可能timer触发在屏幕刷新之前一点,紧接着下次动画,看起来像是屏幕跳动了。
-
所以NSTimer一般用来定时一些非界面刷新的操作就可以了,界面刷新的操作用CADisplayLink比较好一点。
二、使用CADisplayLink来做定时器动画
- 因此我们可以使用CADisplayLink来实现基于定时器的动画,CADisplayLink是Core Animation提供的一个类似于NSTimer的类,它的触发时机是每次屏幕完成刷新之前,也就是说每次屏幕要刷新的时候它都会被触发一次,因此我们就可以使用它来保证动画帧率的连续性,从而减少动画掉帧的几率,使得动画更加平滑。
举例:
(清单4.1)
//
// ViewController.m
// CoreAnimation
//
// Created by 意一yiyi on 2017/11/13.
// Copyright © 2017年 意一yiyi. All rights reserved.
//
#import "ViewController.h"
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define kScreenHeight [UIScreen mainScreen].bounds.size.height
@interface ViewController ()
@property (strong, nonatomic) UIView *contentView;
@property (strong, nonatomic) CAShapeLayer *layer1;
@property (strong, nonatomic) CADisplayLink *displayLink;// CADisplayLink和NSTimer差不多,注意事项也差不多
@property (assign, nonatomic) float waveAmplitude;// 振幅
@property (assign, nonatomic) float waveSpeed;// 波纹流动的速度
@property (assign, nonatomic) float waveOffset;// 初相
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initialize];
[self layoutUI];
[self startWave];
}
#pragma mark - private method
- (void)startWave {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(wave:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)stopWave {
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)wave:(CADisplayLink *)displayLink {
self.waveOffset += self.waveSpeed;
// 绘制第一条贝塞尔曲线
UIBezierPath *path1 = [[UIBezierPath alloc] init];
// 起始点
[path1 moveToPoint:CGPointMake(0, self.waveAmplitude)];// 起始点
// 遍历所有的x坐标,根据公式求出所有的y坐标,从而得到所有的点,贝塞尔曲线将它们连起来
for (CGFloat x = 0.0; x < kScreenWidth; x ++) {
CGFloat y = self.waveAmplitude * sinf(3 * M_PI * x / kScreenWidth + self.waveOffset * M_PI / kScreenWidth);
[path1 addLineToPoint:CGPointMake(x, y)];
}
// 将贝塞尔曲线赋值给shapeLayer的path就可以了
self.layer1.path = path1.CGPath;
}
#pragma mark - layoutUI
- (void)layoutUI {
[self.view addSubview:self.contentView];
[self.contentView.layer addSublayer:self.layer1];
}
#pragma mark - setter, getter
- (UIView *)contentView {
if (_contentView == nil) {
_contentView = [[UIView alloc] init];
_contentView.frame = CGRectMake(0, kScreenHeight - 200, kScreenWidth, 200);
_contentView.backgroundColor = [UIColor yellowColor];
}
return _contentView;
}
- (CAShapeLayer *)layer1 {
if (_layer1 == nil) {
_layer1 = [CAShapeLayer layer];
_layer1.strokeColor = [UIColor redColor].CGColor;
_layer1.fillColor = [UIColor clearColor].CGColor;
_layer1.lineWidth = 5;
_layer1.lineCap = kCALineCapRound;
_layer1.lineJoin = kCALineJoinRound;
}
return _layer1;
}
#pragma mark - initialize
- (void)initialize {
// 初始值
self.waveAmplitude = 20;
self.waveSpeed = 2.0;
self.waveOffset = 0.0;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
1.gif
三、计算每帧的持续时间
但即便是使用了CADisplayLink,我们也不能保证动画就不掉帧了,因为有可能一帧动画的执行时长有可能超过1/60秒,这种情况下我们就需要自己计算每帧的持续时间,代替硬编码的1/60秒了。具体学习可参看原书第11章。