iOS 動(dòng)畫 第十章 基于定時(shí)器的動(dòng)畫

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

定時(shí)幀

//iOS按照每秒60次刷新屏幕,然后CAAnimation計(jì)算出需要展示的新的幀,然后在每次屏幕更新的時(shí)候同步繪制上去,CAAnimation最機(jī)智的地方在于每次刷新需要展示的時(shí)候去計(jì)算插值和緩沖。

//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;
    }
}

//對(duì)主線程,這些任務(wù)包含如下幾項(xiàng)
//處理觸摸事件
//發(fā)送和接受網(wǎng)絡(luò)數(shù)據(jù)包
//執(zhí)行使用gcd的代碼
//處理計(jì)時(shí)器行為
//屏幕重繪

//我們可以通過(guò)一些途徑來(lái)優(yōu)化:
//我們可以用CADisplayLink讓更新頻率嚴(yán)格控制在每次屏幕刷新之后。
//基于真實(shí)幀的持續(xù)時(shí)間而不是假設(shè)的更新頻率來(lái)做動(dòng)畫。
//調(diào)整動(dòng)畫計(jì)時(shí)器的run loop模式,這樣就不會(huì)被別的事件干擾。

CADisplayLink

//CADisplayLink是CoreAnimation提供的另一個(gè)類似于NSTimer的類,它總是在屏幕完成一次更新之前啟動(dòng)
//CADisplayLink有一個(gè)整型的frameInterval屬性,指定了間隔多少幀之后才執(zhí)行。默認(rèn)值是1,意味著每次屏幕更新之前都會(huì)執(zhí)行一次,如果動(dòng)畫的代碼執(zhí)行起來(lái)超過(guò)了六十分之一秒,你可以指定frameInterval為2,就是說(shuō)動(dòng)畫每隔一幀執(zhí)行一次(一秒鐘30幀)或者3,也就是一秒鐘20次
//當(dāng)使用NSTimer的時(shí)候,一旦有機(jī)會(huì)計(jì)時(shí)器就會(huì)開(kāi)啟,但是CADisplayLink卻不一樣:如果它丟失了幀,就會(huì)直接忽略它們,然后在下一次更新的時(shí)候接著運(yùn)行。

計(jì)算幀的持續(xù)時(shí)間

//我們可以在每幀開(kāi)始刷新的時(shí)候用CACurrentMediaTime()記錄當(dāng)前時(shí)間,然后和上一幀記錄的時(shí)間去比較。

- (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 - 標(biāo)準(zhǔn)優(yōu)先級(jí)
//NSRunLoopCommonModes - 高優(yōu)先級(jí)
//UITrackingRunLoopMode - 用于UIScrollView和別的控件的動(dòng)畫

//于是我們可以同時(shí)加入NSDefaultRunLoopMode和UITrackingRunLoopMode來(lái)保證它不會(huì)被滑動(dòng)打斷,也不會(huì)被其他UIKit控件動(dòng)畫影響性能,像這樣:
//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模式配置,通過(guò)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項(xiàng)目整合
//包括一個(gè)和Objective-C綁定的“indie”版本 http://chipmunk-physics.net下載它
//cpSpace - 這是所有的物理結(jié)構(gòu)體的容器。它有一個(gè)大小和一個(gè)可選的重力矢量
//cpBody - 它是一個(gè)固態(tài)無(wú)彈力的剛體。它有一個(gè)坐標(biāo),以及其他物理屬性,例如質(zhì)量,運(yùn)動(dòng)和摩擦系數(shù)等等。
//cpShape - 它是一個(gè)抽象的幾何形狀,用來(lái)檢測(cè)碰撞??梢越o結(jié)構(gòu)體添加一個(gè)多邊形,而且cpShape有各種子類來(lái)代表不同形狀的類型。 在例子中,我們來(lái)對(duì)一個(gè)木箱建模,然后在重力的影響下下落。我們來(lái)創(chuàng)建一個(gè)Crate類,包含屏幕上的可視效果(一個(gè)UIImageView)和一個(gè)物理模型(一個(gè)cpBody和一個(gè)cpPolyShape,一個(gè)cpShape的多邊形子類來(lái)代表矩形木箱)。

//Chipmunk使用了一個(gè)和UIKit顛倒的坐標(biāo)系(Y軸向上為正方向)。為了使得物理模型和視圖之間的同步更簡(jiǎn)單,我們需要通過(guò)使用geometryFlipped屬性翻轉(zhuǎn)容器視圖的集合坐標(biāo)(第3章中有提到),于是模型和視圖都共享一個(gè)相同的坐標(biāo)系。

/*
#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);
}
 */

添加用戶交互

//不想讓這個(gè)邊框矩形滑出屏幕或者被一個(gè)下落的木箱擊中而消失,可以通過(guò)給cpSpace添加四個(gè)cpSegmentShape對(duì)象(cpSegmentShape代表一條直線,所以四個(gè)拼起來(lái)就是一個(gè)矩形)。然后賦給空間的staticBody屬性(一個(gè)不被重力影響的結(jié)構(gòu)體),而不是像木箱那樣一個(gè)新的cpBody實(shí)例

/*
- (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));
}
 
 */

模擬時(shí)間以及固定的時(shí)間步長(zhǎng)

//通過(guò)每次CADisplayLink的啟動(dòng)來(lái)通知屏幕將要刷新,然后記錄下當(dāng)前的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);
}
 */

避免死亡螺旋

//如果幀刷新的時(shí)間延遲的話會(huì)變得很糟糕,我們的模擬需要執(zhí)行更多的次數(shù)來(lái)同步真實(shí)的時(shí)間。這些額外的步驟就會(huì)繼續(xù)延遲幀的更新,等等。這就是所謂的死亡螺旋,因?yàn)樽詈蟮慕Y(jié)果就是幀率變得越來(lái)越慢,直到最后應(yīng)用程序卡死了。
//只要保證你給容錯(cuò)留下足夠的邊長(zhǎng),然后在期望支持的最慢的設(shè)備上進(jìn)行測(cè)試就可以了。如果物理計(jì)算超過(guò)了模擬時(shí)間的50%,就需要考慮增加模擬時(shí)間步長(zhǎng)(或者簡(jiǎn)化場(chǎng)景)。如果模擬時(shí)間步長(zhǎng)增加到超過(guò)1/60秒(一個(gè)完整的屏幕更新時(shí)間),你就需要減少動(dòng)畫幀率到一秒30幀或者增加CADisplayLink的frameInterval來(lái)保證不會(huì)隨機(jī)丟幀,不然你的動(dòng)畫將會(huì)看起來(lái)不平滑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容