iOS 动画 第十章 基于定时器的动画

//nstimer
    //[self timerTest];
    
    //cadisplaylink
//    [self displayLinkTest];
    
    //chipmunk
    //[self chipmunkTest];

定时帧

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

//NSTimer

- (void)timerTest {
    containerView = [[UIView alloc] init];
    containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
    containerView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:containerView];
    
    //add image view
    imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(0.0, 0.0f, 30.0, 30.0f);
    imageView.image = [UIImage imageNamed:@"Star"];
    [containerView addSubview:imageView];
    
    //animate
//    [self pointToLineAnimation];
    [self timerAnimation];
}

- (void)timerAnimation {
    //reset ball to top of screen
    imageView.center = CGPointMake(150, 32);
    //configure the animation
    duration = 1.0;
    timeOffset = 0.0;
    fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [timer invalidate];
    //start the timer
    timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0f
                                             target:self
                                           selector:@selector(timerAction:)
                                           userInfo:nil
                                            repeats:YES];
    
}

- (void)timerAction:(NSTimer *)step {
    //update time offset
    timeOffset = MIN(timeOffset + 1/60.0, duration);
    //get normalized time offset (in range 0-1)
    float time = timeOffset / duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:fromValue
                                    toValure:toValue
                                        time:time];
    //move ball view to new position
    imageView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (timeOffset >= duration) {
        [timer invalidate];
        timer = nil;
    }
}

//对主线程,这些任务包含如下几项
//处理触摸事件
//发送和接受网络数据包
//执行使用gcd的代码
//处理计时器行为
//屏幕重绘

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

CADisplayLink

//CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动
//CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次,如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次
//当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

计算帧的持续时间

//我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。

- (void)displayLinkTest {
    containerView = [[UIView alloc] init];
    containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
    containerView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:containerView];
    
    //add image view
    imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(0.0, 0.0f, 30.0, 30.0f);
    imageView.image = [UIImage imageNamed:@"Star"];
    [containerView addSubview:imageView];
    
    //animate
    //[self timerAnimation];
    [self displayLinkAnimation];
}

- (void)displayLinkAnimation {
    //reset ball to top of screen
    imageView.center = CGPointMake(150.0, 32);
    //configure the animation
    duration2 = 1.0f;timeOffset2 = 0.0;
    fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [timer2 invalidate];
    //start the timer2
    lastStep = CACurrentMediaTime();
    timer2 = [CADisplayLink displayLinkWithTarget:self
                                         selector:@selector(timer2Action:)];
    [timer2 addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)timer2Action:(CADisplayLink *)timer {
    //calculate time delta
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - lastStep;
    lastStep = thisStep;
    //upadate time offset
    timeOffset2 = MIN(timeOffset2 + stepDuration, duration2);
    //get normalized time offset (in range 0 - 1)
    float time = timeOffset2 / duration2;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:fromValue toValure:toValue time:time];
    //move ball view to new position
    imageView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (timeOffset2 >= duration2) {
        [timer2 invalidate];
        timer2 = nil;
    }
}

Run Loop 模式

//run loop模式如下
//NSDefaultRunLoopMode - 标准优先级
//NSRunLoopCommonModes - 高优先级
//UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画

//于是我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:
//self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
//[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];

//NSTimer同样也可以使用不同的run loop模式配置,通过self.timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

物理模拟

//Chipmunk:我们将要使用的物理引擎叫做Chipmunk。Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合
//包括一个和Objective-C绑定的“indie”版本 http://chipmunk-physics.net下载它
//cpSpace - 这是所有的物理结构体的容器。它有一个大小和一个可选的重力矢量
//cpBody - 它是一个固态无弹力的刚体。它有一个坐标,以及其他物理属性,例如质量,运动和摩擦系数等等。
//cpShape - 它是一个抽象的几何形状,用来检测碰撞。可以给结构体添加一个多边形,而且cpShape有各种子类来代表不同形状的类型。 在例子中,我们来对一个木箱建模,然后在重力的影响下下落。我们来创建一个Crate类,包含屏幕上的可视效果(一个UIImageView)和一个物理模型(一个cpBody和一个cpPolyShape,一个cpShape的多边形子类来代表矩形木箱)。

//Chipmunk使用了一个和UIKit颠倒的坐标系(Y轴向上为正方向)。为了使得物理模型和视图之间的同步更简单,我们需要通过使用geometryFlipped属性翻转容器视图的集合坐标(第3章中有提到),于是模型和视图都共享一个相同的坐标系。

/*
#define GRAVITY 1000
- (void)chipmunkTest {
    containerView = [[UIView alloc] init];
    containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
    containerView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:containerView];

    //invert view coordinate system to match physics
    containerView.layer.geometryFlipped = YES;
    //set up physics space
    space = cpSpaceNew();
    cpSpaceSetGravity(space, cpv(0, -GRAVITY));
    
    //add a crate
    Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)];
    [containerView addSubview:crate];
    
    cpSpaceAddBody(space, crate.body);
    cpSpaceAddShape(space, crate.shape);
    //start the timer
    lastStep = CACurrentMediaTime();
    timer2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
    [timer2 addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

void updateShape(cpShape *shape, void *unused) {
    //get the create object associated with the shape
//    Crate *crate = (__bridge Crate *)shape->data;
    Crate *crate = (__bridge Crate *)shape;
    //update crate view position and angle to match physics shape
//     cpBody *body = shape->body;
    cpBody *body = crate.body;
//    crate.center = cpBodyGetPos(body);
    crate.center = CGPointMake(0, 0);
    crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
}
- (void)step:(CADisplayLink *)timer
{
    //calculate step duration
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - lastStep;
    lastStep = thisStep;
    //update physics
    cpSpaceStep(space, stepDuration);
    //update all the shapes
    cpSpaceEachShape(space, &updateShape, NULL);
}
 */

添加用户交互

//不想让这个边框矩形滑出屏幕或者被一个下落的木箱击中而消失,可以通过给cpSpace添加四个cpSegmentShape对象(cpSegmentShape代表一条直线,所以四个拼起来就是一个矩形)。然后赋给空间的staticBody属性(一个不被重力影响的结构体),而不是像木箱那样一个新的cpBody实例

/*
- (void)addCrateWithFrame:(CGRect)frame
{
    Crate *crate = [[Crate alloc] initWithFrame:frame];
    [self.containerView addSubview:crate];
    cpSpaceAddBody(self.space, crate.body);
    cpSpaceAddShape(self.space, crate.shape);
}
- (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end
{
    cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1);
    cpShapeSetCollisionType(wall, 2);
    cpShapeSetFriction(wall, 0.5);
    cpShapeSetElasticity(wall, 0.8);
    cpSpaceAddStaticShape(self.space, wall);
}

- (void)chipmunkTest {
    containerView = [[UIView alloc] init];
    containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
    containerView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:containerView];
    
    //invert view coordinate system to match physics
    self.containerView.layer.geometryFlipped = YES;
    //set up physics space
    self.space = cpSpaceNew();
    cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
    //add wall around edge of view
    [self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)];
    [self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)];
    [self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)];
    [self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];
    //add a crates
    [self addCrateWithFrame:CGRectMake(0, 0, 32, 32)];
    [self addCrateWithFrame:CGRectMake(32, 0, 32, 32)];
    [self addCrateWithFrame:CGRectMake(64, 0, 64, 64)];
    [self addCrateWithFrame:CGRectMake(128, 0, 32, 32)];
    [self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];
    //start the timer
    self.lastStep = CACurrentMediaTime();
    self.timer = [CADisplayLink displayLinkWithTarget:self
                                             selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
                     forMode:NSDefaultRunLoopMode];
    //update gravity using accelerometer
    [UIAccelerometer sharedAccelerometer].delegate = self;
    [UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0;
}

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
    //update gravity
    cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, -acceleration.x * GRAVITY));
}
 
 */

模拟时间以及固定的时间步长

//通过每次CADisplayLink的启动来通知屏幕将要刷新,然后记录下当前的CACurrentMediaTime()。

/*
#define SIMULATION_STEP (1/120.0)
- (void)step:(CADisplayLink *)timer
{
    //calculate frame step duration
    CFTimeInterval frameTime = CACurrentMediaTime();
    //update simulation
    while (lastStep < frameTime) {
        cpSpaceStep(space, SIMULATION_STEP);
        lastStep += SIMULATION_STEP;
    }
    
    //update all the shapes
    cpSpaceEachShape(space, &updateShape, NULL);
}
 */

避免死亡螺旋

//如果帧刷新的时间延迟的话会变得很糟糕,我们的模拟需要执行更多的次数来同步真实的时间。这些额外的步骤就会继续延迟帧的更新,等等。这就是所谓的死亡螺旋,因为最后的结果就是帧率变得越来越慢,直到最后应用程序卡死了。
//只要保证你给容错留下足够的边长,然后在期望支持的最慢的设备上进行测试就可以了。如果物理计算超过了模拟时间的50%,就需要考虑增加模拟时间步长(或者简化场景)。如果模拟时间步长增加到超过1/60秒(一个完整的屏幕更新时间),你就需要减少动画帧率到一秒30帧或者增加CADisplayLink的frameInterval来保证不会随机丢帧,不然你的动画将会看起来不平滑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容