在2007年,喬布斯在第一次介紹 iPhone 的時(shí)候,iPhone 的觸摸屏交互簡(jiǎn)直就像是一種魔法。最好的例子就是在他第一次滑動(dòng) TableView 的展示上。你可以感受到當(dāng)時(shí)觀眾的反應(yīng)是多么驚訝,但是對(duì)于現(xiàn)在的我們來(lái)說(shuō)早已習(xí)以為常。在展示的后面一部分,他特別指出當(dāng)他給別人看了這個(gè)滑動(dòng)例子,別人說(shuō)的一句話(huà): “當(dāng)這個(gè)界面滑動(dòng)的時(shí)候我就已經(jīng)被征服了”.
是什么樣的滑動(dòng)能讓人有‘哇哦’的效果呢?
滑動(dòng)是最完美地展示了通過(guò)觸摸屏直接操作的例子。滾動(dòng)視圖遵從于你的手指,當(dāng)你的手指離開(kāi)屏幕的時(shí),視圖會(huì)自然地繼續(xù)滑動(dòng)直到該停止的時(shí)候停止。它用自然的方式減速,甚至在快到界限的時(shí)候也能表現(xiàn)出細(xì)膩的彈力效果?;瑒?dòng)在任何時(shí)候都保持相應(yīng),并且看上去非常真實(shí)。
在 iOS 中的大部分動(dòng)畫(huà)仍然沒(méi)有按照最初 iPhone 指定的滑動(dòng)標(biāo)準(zhǔn)實(shí)現(xiàn)。這里有很多動(dòng)畫(huà)一旦它們運(yùn)行就不能交互(比如說(shuō)解鎖動(dòng)畫(huà),主界面中打開(kāi)文件夾和關(guān)閉文件夾的動(dòng)畫(huà),和導(dǎo)航欄切換的動(dòng)畫(huà),還有很多)。
然而現(xiàn)在有一些應(yīng)用給我一種始終在控制動(dòng)畫(huà)的體驗(yàn),我們可以直接操作那些我在用的動(dòng)畫(huà)。當(dāng)我們將這些應(yīng)用和其他的應(yīng)用相比較之后,我們就能感覺(jué)到明顯的區(qū)別。這些應(yīng)用中最優(yōu)秀的有最初的 Twitter iPad app, 和現(xiàn)在的 Facebook Paper。但目前,使用直接操作為主并且可以中斷動(dòng)畫(huà)的應(yīng)用仍然很少。這就給我們做出更好的應(yīng)用提供了機(jī)會(huì),讓我們的應(yīng)用有更不同的,更高質(zhì)量的體驗(yàn)。
當(dāng)我們用 UIView 或者 CAAnimation 來(lái)實(shí)現(xiàn)交互式動(dòng)畫(huà)時(shí)會(huì)有兩個(gè)大問(wèn)題: 這些動(dòng)畫(huà)會(huì)將你在屏幕上的內(nèi)容和 layer 上的實(shí)際的特定屬性分離開(kāi)來(lái),并且他們直接操作這些特定屬性。
Core Animation 是通過(guò)分離 layer 的模型屬性和你在屏幕上看到的界面 (顯示層) 的方式來(lái)設(shè)計(jì)的,這就導(dǎo)致我們很難去創(chuàng)建一個(gè)可以在任何時(shí)候能交互的動(dòng)畫(huà),因?yàn)樵趧?dòng)畫(huà)時(shí),模型和界面已經(jīng)不能匹配了。這時(shí),我們不得不通過(guò)手動(dòng)的方式來(lái)同步這兩個(gè)的狀態(tài),來(lái)達(dá)到改變動(dòng)畫(huà)的效果:
view.layer.center = view.layer.presentationLayer.center;
[view.layer removeAnimationForKey:@"animation"];
// 添加新動(dòng)畫(huà)
CAAnimation
動(dòng)畫(huà)的更大的問(wèn)題是它們是直接在 layer 上對(duì)屬性進(jìn)行操作的。這意味著什么呢?比如我們想指定一個(gè) layer 從坐標(biāo)為 (100, 100) 的位置運(yùn)動(dòng)到 (300, 300) 的位置,但是在它運(yùn)動(dòng)到中間的時(shí)候,我們想它停下來(lái)并且讓它回到它原來(lái)的位置,事情就變得非常復(fù)雜了。如果你只是簡(jiǎn)單地刪除當(dāng)前的動(dòng)畫(huà)然后再添加一個(gè)新的,那么這個(gè) layer 的速率就會(huì)不連續(xù)。
http://wiki.jikexueyuan.com/project/objc/images/12-29.png" alt="" />
然而,我們想要的是一個(gè)漂亮的,流暢地減速和加速的動(dòng)畫(huà)。
http://wiki.jikexueyuan.com/project/objc/images/12-30.png" alt="" />
只有通過(guò)間接操作動(dòng)畫(huà)才能達(dá)到上面的效果,比如通過(guò)模擬力在界面上的表現(xiàn)。新的動(dòng)畫(huà)需要用 layer 的當(dāng)前速度矢量作為參數(shù)傳入來(lái)達(dá)到流暢的效果。
看一下 UIView 中關(guān)于彈簧動(dòng)畫(huà)的 API (animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:
),你會(huì)注意到速率是個(gè) CGFloat
。所以當(dāng)我們給一個(gè)移動(dòng) view 的動(dòng)畫(huà)在其運(yùn)動(dòng)的方向上加一個(gè)初始的速率時(shí),你沒(méi)法告知?jiǎng)赢?huà)這個(gè) view 現(xiàn)在的運(yùn)動(dòng)狀態(tài),比如我們不知道要添加的動(dòng)畫(huà)的方向是不是和原來(lái)的 view 的速度方向垂直。為了使其成為可能,這個(gè)速度需要用向量來(lái)表示。
讓我們看一下我們?cè)鯓觼?lái)正確實(shí)現(xiàn)一個(gè)可交互并且可以中斷的動(dòng)畫(huà)。我們來(lái)做一個(gè)類(lèi)似于控制中心板的東西來(lái)實(shí)現(xiàn)這個(gè)效果:
http://wiki.jikexueyuan.com/project/objc/images/12-31.gif" alt="" />
這個(gè)控制板有兩個(gè)狀態(tài):打開(kāi)和關(guān)閉。你可以通過(guò)點(diǎn)擊來(lái)切換這兩個(gè)狀態(tài),或者通過(guò)上下拖動(dòng)來(lái)調(diào)調(diào)整它向上或向下。我要將這個(gè)控制面板的所有狀態(tài)都做到可以交互,甚至是在動(dòng)畫(huà)的過(guò)程中也可以,這是一個(gè)很大的挑戰(zhàn)。比如,當(dāng)你在這個(gè)控制板還沒(méi)有切換到打開(kāi)狀態(tài)的動(dòng)畫(huà)過(guò)程中,你點(diǎn)擊了它,那么它應(yīng)該從現(xiàn)在這個(gè)點(diǎn)的位置馬上回到關(guān)閉狀態(tài)的位置。在現(xiàn)在很多的應(yīng)用中,大部分都是用默認(rèn)的動(dòng)畫(huà) API,你必須要等一個(gè)動(dòng)畫(huà)結(jié)束之后你才能做自己想做的事情。或者,如果你不等待的話(huà),就會(huì)看到一個(gè)不連續(xù)的速度曲線(xiàn)。我們要解決這個(gè)問(wèn)題。
隨著 iOS7 的發(fā)布,蘋(píng)果向我們展示了一個(gè)叫 UIKit 力學(xué)的動(dòng)畫(huà)框架 (可以參見(jiàn) WWDC 2013 sessions 206 和 221)。UIKit 力學(xué)是一個(gè)基于模擬物理引擎的框架,只要你添加指定的行為到動(dòng)畫(huà)對(duì)象上來(lái)實(shí)現(xiàn) UIDynamicItem 協(xié)議就能實(shí)現(xiàn)很多動(dòng)畫(huà)。這個(gè)框架非常強(qiáng)大,并且它能夠在多個(gè)物體間啟用像是附著和碰撞這樣的復(fù)雜行為。請(qǐng)看一下 UIKit Dynamics Catalog,確認(rèn)一下什么是可用的。
因?yàn)?UIKit 力學(xué)中的的動(dòng)畫(huà)是被間接驅(qū)動(dòng)的,就像我在上面提到的,這使我們實(shí)現(xiàn)真實(shí)的交互式動(dòng)畫(huà)成為可能,它能在任何時(shí)候被中斷并且擁有連續(xù)的加速度。同時(shí),UIKit 力學(xué)在物理層的抽象上能完全勝任我們一般情況下在用戶(hù)界面中的所需要的所有動(dòng)畫(huà)。其實(shí)在大部分情況下,我們只會(huì)用到其中的一小部分功能。
為了實(shí)現(xiàn)我們的控制板的行為,我們將使用 UIkit 力學(xué)中的兩個(gè)不同行為:UIAttachmentBehavior 和 UIDynamicItemBehavior。附著行為用來(lái)扮演彈簧的角色,它將界面向目標(biāo)點(diǎn)拉動(dòng)。另一方面,我們用動(dòng)態(tài) item behvaior 定義了比如摩擦系數(shù)這樣的界面的內(nèi)置屬性。
我創(chuàng)建了一個(gè)我們自己的行為子類(lèi),以將這兩個(gè)行為封裝到我們的控制板上:
@interface PaneBehavior : UIDynamicBehavior
@property (nonatomic) CGPoint targetPoint;
@property (nonatomic) CGPoint velocity;
- (instancetype)initWithItem:(id <UIDynamicItem>)item;
@end
我們通過(guò)一個(gè) dynamic item 來(lái)初始化這個(gè)行為,然后就可以設(shè)置它的目標(biāo)點(diǎn)和我們想要的任何速度。在內(nèi)部,我們創(chuàng)建了附著行為和 dynamic item 行為,并且將這些行為添加到我們自定義的行為中:
- (void)setup
{
UIAttachmentBehavior *attachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.item attachedToAnchor:CGPointZero];
attachmentBehavior.frequency = 3.5;
attachmentBehavior.damping = .4;
attachmentBehavior.length = 0;
[self addChildBehavior:attachmentBehavior];
self.attachmentBehavior = attachmentBehavior;
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.item]];
itemBehavior.density = 100;
itemBehavior.resistance = 10;
[self addChildBehavior:itemBehavior];
self.itemBehavior = itemBehavior;
}
為了用 targetPoint
和 velocity
屬性來(lái)影響 item 的 behavior,我們需要重寫(xiě)它們的 setter 方法,并且分別修改在附著行為和 item behaviors 中的對(duì)應(yīng)的屬性。我們對(duì)目標(biāo)點(diǎn)的 setter 方法來(lái)說(shuō),這個(gè)改動(dòng)很簡(jiǎn)單:
- (void)setTargetPoint:(CGPoint)targetPoint
{
_targetPoint = targetPoint;
self.attachmentBehavior.anchorPoint = targetPoint;
}
對(duì)于 velocity
屬性,我們需要多做一些工作,因?yàn)?dynamic item behavior 只允許相對(duì)地改變速度。這就意味如果我們要將 velocity
設(shè)置為絕對(duì)值,首先我們就需要得到當(dāng)前的速度,然后再加上速度差才能得到我們的目標(biāo)速度。
- (void)setVelocity:(CGPoint)velocity
{
_velocity = velocity;
CGPoint currentVelocity = [self.itemBehavior linearVelocityForItem:self.item];
CGPoint velocityDelta = CGPointMake(velocity.x - currentVelocity.x, velocity.y - currentVelocity.y);
[self.itemBehavior addLinearVelocity:velocityDelta forItem:self.item];
}
我們的控制板有三個(gè)不同狀態(tài):在開(kāi)始或結(jié)束位置的靜止?fàn)顟B(tài),正在被用戶(hù)拖動(dòng)的狀態(tài),以及在沒(méi)有用戶(hù)控制時(shí)運(yùn)動(dòng)到結(jié)束位置的動(dòng)畫(huà)狀態(tài)。
為了將從直接操作狀態(tài) (用戶(hù)拖動(dòng)這個(gè)滑動(dòng)板) 過(guò)渡到動(dòng)畫(huà)狀態(tài)這個(gè)過(guò)程做的流暢,我們還有很多其他的事要做。當(dāng)用戶(hù)停止拖動(dòng)控制板時(shí),它會(huì)發(fā)送一個(gè)消息到它的 delegate。根據(jù)這個(gè)方法,我們可以知道這個(gè)板應(yīng)該朝哪個(gè)方向運(yùn)動(dòng),然后在我們自定義的 PaneBehavior
上設(shè)置結(jié)束點(diǎn),以及初始速度 (這非常重要),并將行為添加到動(dòng)畫(huà)器中去,以此確保從拖動(dòng)操作到動(dòng)畫(huà)狀態(tài)這個(gè)過(guò)程能夠非常流暢。
- (void)draggableView:(DraggableView *)view draggingEndedWithVelocity:(CGPoint)velocity
{
PaneState targetState = velocity.y >= 0 ? PaneStateClosed : PaneStateOpen;
[self animatePaneToState:targetState initialVelocity:velocity];
}
- (void)animatePaneToState:(PaneState)targetState initialVelocity:(CGPoint)velocity
{
if (!self.paneBehavior) {
PaneBehavior *behavior = [[PaneBehavior alloc] initWithItem:self.pane];
self.paneBehavior = behavior;
}
self.paneBehavior.targetPoint = [self targetPointForState:targetState];
if (!CGPointEqualToPoint(velocity, CGPointZero)) {
self.paneBehavior.velocity = velocity;
}
[self.animator addBehavior:self.paneBehavior];
self.paneState = targetState;
}
一旦用戶(hù)用他的手指再次觸動(dòng)控制板時(shí),我必須要將所有的 dynamic behavior 從 animator 刪除,這樣才不會(huì)影響控制板對(duì)拖動(dòng)手勢(shì)的響應(yīng):
- (void)draggableViewBeganDragging:(DraggableView *)view
{
[self.animator removeAllBehaviors];
}
我們不僅僅允許控制板可以被拖動(dòng),還要允許它可以被點(diǎn)擊,讓它可以從一個(gè)位置跳轉(zhuǎn)到另一個(gè)位置以達(dá)到開(kāi)關(guān)的效果。一旦點(diǎn)擊事件發(fā)生,我們就會(huì)立即調(diào)整這個(gè)滑動(dòng)板的目標(biāo)位置。因?yàn)槲覀儾荒苤苯涌刂苿?dòng)畫(huà),但是通過(guò)彈力和摩擦力,我們的動(dòng)畫(huà)可以非常流暢地執(zhí)行這個(gè)動(dòng)作:
- (void)didTap:(UITapGestureRecognizer *)tapRecognizer
{
PaneState targetState = self.paneState == PaneStateOpen ? PaneStateClosed : PaneStateOpen;
[self animatePaneToState:targetState initialVelocity:CGPointZero];
}
這樣就實(shí)現(xiàn)了我們的大部分功能了。你可以在 GitHub 上查看完整的例子。
重申一點(diǎn):UIKit 力學(xué)可以通過(guò)在界面上模擬力來(lái)間接地驅(qū)動(dòng)動(dòng)畫(huà)(我們的例子中,使用的是彈力和摩擦力)。這間接地使我們?cè)谌魏螘r(shí)候都能以連續(xù)的速度曲線(xiàn)來(lái)與界面進(jìn)行交互。
現(xiàn)在我們已經(jīng)通過(guò) UIKit 力學(xué)實(shí)現(xiàn)了整個(gè)交互,讓我們回顧一下這個(gè)場(chǎng)景。這個(gè)例子的動(dòng)畫(huà)中我們只用了 UIKit 力學(xué)中一小部分功能,并且它的實(shí)現(xiàn)方式也非常簡(jiǎn)單。對(duì)于我們來(lái)說(shuō)這是一個(gè)去理解它其中的過(guò)程的很好的例子,但是如果我們使用的環(huán)境中沒(méi)有 UIKit 力學(xué) (比如說(shuō)在 Mac 上),或者你的使用場(chǎng)景中不能很好的適用 UIKit 力學(xué)呢。
至于在你的應(yīng)用中大部分時(shí)間會(huì)用的動(dòng)畫(huà),比如簡(jiǎn)單的彈力動(dòng)畫(huà),我們控制它真的不難。我們可以做一個(gè)練習(xí),來(lái)看看如何拋棄 UIKit 力學(xué)這個(gè)巨大的黑盒子,看要如何“手動(dòng)”來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的交互。想法非常簡(jiǎn)單:我們只要每秒修改這個(gè) view 的 frame 60 次。每一幀我們都基于當(dāng)前速度和作用在 view 上的力來(lái)調(diào)整 view 的 frame 就可以了。
首先讓我們看一下我們需要知道的基礎(chǔ)物理知識(shí),這樣我們才能實(shí)現(xiàn)出剛才使用 UIKit 力學(xué)實(shí)現(xiàn)的那種彈簧動(dòng)畫(huà)效果。為了簡(jiǎn)化問(wèn)題,雖然引入第二個(gè)維度也是很直接的,但我們?cè)谶@里只關(guān)注一維的情況 (在我們的例子中就是這樣的情況)。
我們的目標(biāo)是依據(jù)控制面板當(dāng)前的位置和上一次動(dòng)畫(huà)后到現(xiàn)在為止經(jīng)過(guò)的時(shí)間,來(lái)計(jì)算它的新位置。我們可以把它表示成這樣:
y = y0 + Δy
位置的偏移量可以通過(guò)速度和時(shí)間的函數(shù)來(lái)表達(dá):
Δy = v ? Δt
這個(gè)速度可以通過(guò)前一次的速度加上速度偏移量算出來(lái),這個(gè)速度偏移量是由力在 view 上的作用引起的。
v = v0 + Δv
速度的變化可以通過(guò)作用在這個(gè) view 上的沖量計(jì)算出來(lái):
Δv = (F ? Δt) / m
現(xiàn)在,讓我們看一下作用在這個(gè)界面上的力。為了得到彈簧效果,我們必須要將摩擦力和彈力結(jié)合起來(lái):
F = F_spring + F_friction
彈力的計(jì)算方法我們可以從任何一本教科書(shū)中得到 (編者注:簡(jiǎn)單的胡克定律):
F_spring = k ? x
k
是彈簧的勁度系數(shù),x
是 view 到目標(biāo)結(jié)束位置的距離 (也就是彈簧的長(zhǎng)度)。因此,我們可以把它寫(xiě)成這樣:
F_spring = k ? abs(y_target - y0)
摩擦力和 view 的速度成正比:
F_friction = μ ? v
μ
是一個(gè)簡(jiǎn)單的摩擦系數(shù)。你可以通過(guò)別的方式來(lái)計(jì)算摩擦力,但是這個(gè)方法能很好地做出我們想要的動(dòng)畫(huà)效果。
將上面的表達(dá)式放在一起,我們就可以算出作用在界面上的力:
F = k ? abs(y_target - y0) + μ ? v
為了實(shí)現(xiàn)起來(lái)更簡(jiǎn)單點(diǎn)些,我們將 view 的質(zhì)量設(shè)為 1,這樣我們就能計(jì)算在位置上的變化:
Δy = (v0 + (k ? abs(y_target - y0) + μ ? v) ? Δt) ? Δt
為了實(shí)現(xiàn)這個(gè)動(dòng)畫(huà),我們首先需要?jiǎng)?chuàng)建我們自己的 Animator
類(lèi),它將扮演驅(qū)動(dòng)動(dòng)畫(huà)的角色。這個(gè)類(lèi)使用了 CADisplayLink
,CADisplayLink
是專(zhuān)門(mén)用來(lái)將繪圖與屏幕刷新頻率相同步的定時(shí)器。換句話(huà)說(shuō),如果你的動(dòng)畫(huà)是流暢的,這個(gè)定時(shí)器就會(huì)每秒調(diào)用你的方法60次。接下來(lái),我們需要實(shí)現(xiàn) Animation
協(xié)議來(lái)和我們的 Animator
一起工作。這個(gè)協(xié)議只有一個(gè)方法,animationTick:finished:
。屏幕每次被刷新時(shí)都會(huì)調(diào)用這個(gè)方法,并且在方法中會(huì)得到兩個(gè)參數(shù):第一個(gè)參數(shù)是前一個(gè) frame 的持續(xù)時(shí)間,第二個(gè)參數(shù)是一個(gè)指向 BOOL
的指針。當(dāng)我們?cè)O(shè)置這個(gè)指針的值為 YES
時(shí),我們就可以與 Animator
取得通訊并匯報(bào)動(dòng)畫(huà)完成;
@protocol Animation <NSObject>
- (void)animationTick:(CFTimeInterval)dt finished:(BOOL *)finished;
@end
我們會(huì)在下面實(shí)現(xiàn)這個(gè)方法。首先,根據(jù)時(shí)間間隔我們來(lái)計(jì)算由彈力和摩擦力的合力。然后根據(jù)這個(gè)力來(lái)更新速度,并調(diào)整 view 的中心位置。最后,當(dāng)這個(gè)速度降低并且 view 到達(dá)結(jié)束位置時(shí),我們就停止這個(gè)動(dòng)畫(huà):
- (void)animationTick:(CFTimeInterval)dt finished:(BOOL *)finished
{
static const float frictionConstant = 20;
static const float springConstant = 300;
CGFloat time = (CGFloat) dt;
//摩擦力 = 速度 * 摩擦系數(shù)
CGPoint frictionForce = CGPointMultiply(self.velocity, frictionConstant);
//彈力 = (目標(biāo)位置 - 當(dāng)前位置) * 彈簧勁度系數(shù)
CGPoint springForce = CGPointMultiply(CGPointSubtract(self.targetPoint, self.view.center), springConstant);
//力 = 彈力 - 摩擦力
CGPoint force = CGPointSubtract(springForce, frictionForce);
//速度 = 當(dāng)前速度 + 力 * 時(shí)間 / 質(zhì)量
self.velocity = CGPointAdd(self.velocity, CGPointMultiply(force, time));
//位置 = 當(dāng)前位置 + 速度 * 時(shí)間
self.view.center = CGPointAdd(self.view.center, CGPointMultiply(self.velocity, time));
CGFloat speed = CGPointLength(self.velocity);
CGFloat distanceToGoal = CGPointLength(CGPointSubtract(self.targetPoint, self.view.center));
if (speed < 0.05 && distanceToGoal < 1) {
self.view.center = self.targetPoint;
*finished = YES;
}
}
這就是這個(gè)方法里的全部?jī)?nèi)容。我們把這個(gè)方法封裝到一個(gè) SpringAnimation
對(duì)象中。除了這個(gè)方法之外,這個(gè)對(duì)象中還有一個(gè)初始化方法,它指定了 view 中心的目標(biāo)位置 (在我們的例子中,就是打開(kāi)狀態(tài)時(shí)界面的中心位置,或者關(guān)閉狀態(tài)時(shí)界面的中心位置) 和初始的速度。
我們的 view 類(lèi)剛好和使用 UIDynamic 的例子一樣:它有一個(gè)拖動(dòng)手勢(shì),并且根據(jù)拖動(dòng)手勢(shì)來(lái)更新中心位置。它也有兩個(gè)同樣的 delegate 方法,這兩個(gè)方法會(huì)實(shí)現(xiàn)動(dòng)畫(huà)的初始化。首先,一旦用戶(hù)開(kāi)始拖動(dòng)控制板時(shí),我們就取消所有動(dòng)畫(huà):
- (void)draggableViewBeganDragging:(DraggableView *)view
{
[self cancelSpringAnimation];
}
一旦停止拖動(dòng),我們就根據(jù)從拖動(dòng)手勢(shì)中得到的最后一個(gè)速率值來(lái)開(kāi)始我們的動(dòng)畫(huà)。我們根據(jù)拖動(dòng)狀態(tài) paneState
計(jì)算出動(dòng)畫(huà)的結(jié)束位置:
- (void)draggableView:(DraggableView *)view draggingEndedWithVelocity:(CGPoint)velocity
{
PaneState targetState = velocity.y >= 0 ? PaneStateClosed : PaneStateOpen;
self.paneState = targetState;
[self startAnimatingView:view initialVelocity:velocity];
}
- (void)startAnimatingView:(DraggableView *)view initialVelocity:(CGPoint)velocity
{
[self cancelSpringAnimation];
self.springAnimation = [UINTSpringAnimation animationWithView:view target:self.targetPoint velocity:velocity];
[view.animator addAnimation:self.springAnimation];
}
剩下來(lái)要做的就是添加點(diǎn)擊動(dòng)畫(huà)了,這很簡(jiǎn)單。一旦我們觸發(fā)這個(gè)狀態(tài),就開(kāi)始動(dòng)畫(huà)。如果這里正在進(jìn)行彈簧動(dòng)畫(huà),我們就用當(dāng)時(shí)的速度作為開(kāi)始。如果這個(gè)彈簧動(dòng)畫(huà)是 nil,那么這個(gè)開(kāi)始速度就是 CGPointZero。想要知道為什么依然可以進(jìn)行動(dòng)畫(huà),可以看看 animationTick:finished:
里的代碼。當(dāng)這個(gè)起始速度為 0 的時(shí)候,彈力就會(huì)使速度緩慢地增長(zhǎng),直到面板到達(dá)目標(biāo)位置:
- (void)didTap:(UITapGestureRecognizer *)tapRecognizer
{
PaneState targetState = self.paneState == PaneStateOpen ? PaneStateClosed : PaneStateOpen;
self.paneState = targetState;
[self startAnimatingView:self.pane initialVelocity:self.springAnimation.velocity];
}
最后,我們需要一個(gè) Animator
,也就是動(dòng)畫(huà)的驅(qū)動(dòng)者。Animator 封裝了 display link。因?yàn)槊總€(gè) display link 都鏈接一個(gè)指定的 UIScreen
,所以我們根據(jù)這個(gè)指定的 UIScreen 來(lái)初始化我們的 animator。我們初始化一個(gè) display link,并且將它加入到 run loop 中。因?yàn)楝F(xiàn)在還沒(méi)有動(dòng)畫(huà),所以我們是從暫停狀態(tài)開(kāi)始的:
- (instancetype)initWithScreen:(UIScreen *)screen
{
self = [super init];
if (self) {
self.displayLink = [screen displayLinkWithTarget:self selector:@selector(animationTick:)];
self.displayLink.paused = YES;
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
self.animations = [NSMutableSet new];
}
return self;
}
一旦我們添加了這個(gè)動(dòng)畫(huà),我們要確保這個(gè) display link 不再是停止?fàn)顟B(tài):
- (void)addAnimation:(id<Animation>)animation
{
[self.animations addObject:animation];
if (self.animations.count == 1) {
self.displayLink.paused = NO;
}
}
我們?cè)O(shè)置這個(gè) display link 來(lái)調(diào)用 animationTick:
方法,在每個(gè) Tick 中,我們都遍歷它的動(dòng)畫(huà)數(shù)組,并且給這些動(dòng)畫(huà)數(shù)組中的每個(gè)動(dòng)畫(huà)發(fā)送一個(gè)消息。如果這個(gè)動(dòng)畫(huà)數(shù)組中已經(jīng)沒(méi)有動(dòng)畫(huà)了,我們就暫停這個(gè) display link。
- (void)animationTick:(CADisplayLink *)displayLink
{
CFTimeInterval dt = displayLink.duration;
for (id<Animation> a in [self.animations copy]) {
BOOL finished = NO;
[a animationTick:dt finished:&finished];
if (finished) {
[self.animations removeObject:a];
}
}
if (self.animations.count == 0) {
self.displayLink.paused = YES;
}
}
完整的項(xiàng)目在 GitHub 上。
我們必須記住,通過(guò) display link 來(lái)驅(qū)動(dòng)動(dòng)畫(huà) (就像我們剛才演示的例子,或者我們使用UIkit力學(xué)來(lái)做的例子,又或者是使用 Facebook 的 Pop 框架) 是有代價(jià)需要進(jìn)行權(quán)衡的。就像 Andy Matuschar 指出的那樣,UIView 和 CAAnimation 動(dòng)畫(huà)比其他任務(wù)更少受系統(tǒng)的影響,因?yàn)楸绕鹉愕膽?yīng)用來(lái)說(shuō),渲染處于更高的優(yōu)先級(jí)。
現(xiàn)在 Mac 中還沒(méi)有 UIKit 力學(xué)。如果你想在 Mac 中創(chuàng)建一個(gè)真實(shí)的交互式動(dòng)畫(huà),你必須自己去實(shí)現(xiàn)這些動(dòng)畫(huà)。我們已經(jīng)向你展示了如何在 iOS 中實(shí)現(xiàn)這些動(dòng)畫(huà),所以在 OS X 中實(shí)現(xiàn)相似的功能也是非常簡(jiǎn)單的。你可以查看在 GitHub 中的完整項(xiàng)目,如果你想要應(yīng)用到 OS X 中,這里還有一些地方需要修改:
Animator
。在Mac中沒(méi)有 CADisplayLink
,但是取而代之的有 CVDisplayLink
,它是以 C 語(yǔ)言為基礎(chǔ)的 API。創(chuàng)建它需要做更多的工作,但也是很直接。NSView
類(lèi)沒(méi)有 center 這個(gè)屬性,所以我們用為 frame 中的 origin 做動(dòng)畫(huà)來(lái)代替。mouseDown:
,mouseUp:
和 mouseDragged:
方法。上面就是我們需要在 Mac 中使用我們的動(dòng)畫(huà)效果在代碼所需要做的修改。對(duì)于像這樣的簡(jiǎn)單 view,它能很好的勝任。但對(duì)于更復(fù)雜的動(dòng)畫(huà),你可能就不會(huì)想通過(guò)為 frame 做動(dòng)畫(huà)來(lái)實(shí)現(xiàn)了,我們可以用 transform
來(lái)代替,瀏覽 Jonathan Willing 寫(xiě)的關(guān)于 OS X 動(dòng)畫(huà)的博客,你會(huì)獲益良多。
上個(gè)星期圍繞著 Facebook 的 POP 框架的討論絡(luò)繹不絕。POP 框架是 Paper 應(yīng)用背后支持的動(dòng)畫(huà)引擎。它的操作非常像我們上面講的驅(qū)動(dòng)動(dòng)畫(huà)的例子,但是它以非常靈活的方式巧妙地封裝到了一個(gè)程序包中。
讓我們動(dòng)手用 POP 來(lái)驅(qū)動(dòng)我們的動(dòng)畫(huà)吧。因?yàn)槲覀冏约旱念?lèi)中已經(jīng)封裝了彈簧動(dòng)畫(huà),這些改變就非常簡(jiǎn)單了。我們所要做的就是初始化一個(gè) POP 動(dòng)畫(huà)來(lái)代替我們剛才自己做的動(dòng)畫(huà),并將下面這段代碼加入到 view 中:
- (void)animatePaneWithInitialVelocity:(CGPoint)initialVelocity
{
[self.pane pop_removeAllAnimations];
POPSpringAnimation *animation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewCenter];
animation.velocity = [NSValue valueWithCGPoint:initialVelocity];
animation.toValue = [NSValue valueWithCGPoint:self.targetPoint];
animation.springSpeed = 15;
animation.springBounciness = 6;
[self.pane pop_addAnimation:animation forKey:@"animation"];
self.animation = animation;
}
你可以在 GitHub 中找到使用 POP 框架的完整例子。
讓其工作非常簡(jiǎn)單,并且通過(guò)它我們可以實(shí)現(xiàn)很多更復(fù)雜的動(dòng)畫(huà)。但是它真正強(qiáng)大的地方在于它能夠?qū)崿F(xiàn)真正的可交互和可中斷的動(dòng)畫(huà),就像我們上面提到的那樣,因?yàn)樗苯又С忠运俣茸鳛檩斎雲(yún)?shù)。如果你打算從一開(kāi)始到被中斷這過(guò)程中的任何時(shí)候都能交互,像 POP 這樣的框架就能幫你實(shí)現(xiàn)這些動(dòng)畫(huà),并且它能始終保證動(dòng)畫(huà)一直很平滑。
如果你不滿(mǎn)足于用 POPSpringAnimation
和 POPDecayAnimation
的開(kāi)箱即用的處理方式的話(huà),POP 還提供了 POPCustomAnimation
類(lèi),它基本上是一個(gè) display link 的方便的轉(zhuǎn)換,來(lái)在動(dòng)畫(huà)的每一個(gè) tick 的回調(diào) block 中驅(qū)動(dòng)你自己的動(dòng)畫(huà)。
隨著 iOS7 中從對(duì)擬物化的視覺(jué)效果的遠(yuǎn)離,以及對(duì) UI 行為的關(guān)注,真實(shí)的交互式動(dòng)畫(huà)通向未來(lái)的大道變得越來(lái)越明顯。它們也是將初代 iPhone 中滑動(dòng)行為的魔力延續(xù)到交互的各個(gè)方面的一條康莊大道。為了讓這些魔力成為現(xiàn)實(shí),我們就不能在開(kāi)發(fā)過(guò)程中才想到這些動(dòng)畫(huà),而是應(yīng)該在設(shè)計(jì)時(shí)就要考慮這些交互,這一點(diǎn)非常重要。
非常感謝 Loren Brichter 給這篇文章提出的一些意見(jiàn)。