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来保证不会随机丢帧,不然你的动画将会看起来不平滑。

你可能感兴趣的:(iOS 动画 第十章 基于定时器的动画)