鍍金池/ 教程/ iOS/ 交互式動(dòng)畫(huà)
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫(xiě)腳本
結(jié)構(gòu)體和值類(lèi)型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開(kāi)發(fā)
Collection View 動(dòng)畫(huà)
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫(huà)
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類(lèi)
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫(huà)解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶(hù)端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫(huà)
常見(jiàn)的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類(lèi)
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線(xiàn)程安全類(lèi)的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴(lài)注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪(fǎng)談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫(huà)
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺(jué)
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開(kāi)發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴(lài)注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

交互式動(dòng)畫(huà)

在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í)。

動(dòng)畫(huà)的狀態(tài)

在 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)。

真實(shí)交互式動(dòng)畫(huà)的挑戰(zhà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),并且他們直接操作這些特定屬性。

模型 (Model) 和顯示 (Presentation) 的分離

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à)

直接控制 vs 間接控制

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)題。

UIKit 力學(xué)

隨著 iOS7 的發(fā)布,蘋(píng)果向我們展示了一個(gè)叫 UIKit 力學(xué)的動(dòng)畫(huà)框架 (可以參見(jiàn) WWDC 2013 sessions 206221)。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è)不同行為:UIAttachmentBehaviorUIDynamicItemBehavior。附著行為用來(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;
}

為了用 targetPointvelocity 屬性來(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];
}

將Behavior投入使用

我們的控制板有三個(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é)呢。

自己操作動(dòng)畫(huà)

至于在你的應(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)動(dòng)畫(huà)

為了實(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í)界面的中心位置) 和初始的速度。

將動(dòng)畫(huà)添加到 view 上

我們的 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];
}

動(dòng)畫(huà)驅(qū)動(dòng)

最后,我們需要一個(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 上。

權(quán)衡

我們必須記住,通過(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í)。

回到 Mac

現(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 中,這里還有一些地方需要修改:

  • 第一個(gè)要修改的就是 Animator。在Mac中沒(méi)有 CADisplayLink,但是取而代之的有 CVDisplayLink,它是以 C 語(yǔ)言為基礎(chǔ)的 API。創(chuàng)建它需要做更多的工作,但也是很直接。
  • iOS 中的彈簧動(dòng)畫(huà)是基于調(diào)整 view 的中心位置來(lái)實(shí)現(xiàn)的。而 OS X 中的 NSView 類(lèi)沒(méi)有 center 這個(gè)屬性,所以我們用為 frame 中的 origin 做動(dòng)畫(huà)來(lái)代替。
  • 在 Mac 中是沒(méi)有手勢(shì)識(shí)別,所以我要在我們自定義的 view 子類(lèi)中實(shí)現(xiàn) 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ì)獲益良多。

Facebook 的 POP 框架

上個(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)足于用 POPSpringAnimationPOPDecayAnimation 的開(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à)。

展望未來(lái)

隨著 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)。