轉自http://www.infoq.com/cn/articles/ios-interactive-animation-p2
不久前結束的WWDC 2016 Session 216: Advances in UIKit Animations and Transitions介紹了 iOS 10 的新動畫 API,讓動畫與交互無縫連接,這是「開發者的大事、大快所有人心的大好事」。在上篇我探討了 iOS 10 以下的系統中如何使用 UIView Animation 實現交互動畫,本篇來探討 iOS 10 帶來的變化。
新 API 的改進
新 API 的核心是UIViewPropertyAnimator類,在UIViewAnimating協議中定義了交互動畫需要的所有基礎功能:暫停,恢復,停止,逆轉動畫以及控制動畫進度。UIView Animation 并沒有提供這些功能,這些功能都需要回到 Core Animation 作用的 CALayer 里使用分散且文檔晦澀難懂的 API 來實現。UIViewImplicitlyAnimating協議主要補充了與 UIView Animation 類似的添加動畫 Block 的方法。
UITimingCurveProvider協議重新封裝了時間函數,而UISpringTimingParameters類終于帶來了期待已久的兩點改進:
完全版本的彈簧動畫:iOS 7 引入了簡化的 Spring UIView Animation API,iOS 9 引入了無文檔的完全版本的 Spring Core Animation API;而這兩個版本的初始速度皆為數值,iOS 10 的所有彈簧動畫的速度都是向量。
UIViewPropertyAnimator類可以視為面向對象版本的 UIView Animation,以動畫 Block 為基礎的設計解決了多個 UIView 參與動畫時的交互控制,而使用 UIView Animation 時面對多個視圖參與交互動畫就需要針對每個視圖進行控制。
交互轉場的最后一塊拼圖
在轉場動畫里,非交互轉場與交互轉場之間有著明顯的界限:如果以交互轉場開始,盡管在交互結束后會切換到非交互狀態,但之后無法再次切換到交互狀態,只能等待其結束;如果以非交互轉場開始,在轉場動畫結束前是無法切換到交互控制狀態的,只能等待其結束。iOS 10 在轉場協議中引入了上述 API,這使得非交互轉場與交互轉場之間的界限不再涇渭分明。
讓轉場動畫在非交互狀態與交互狀態之間自由切換很困難,UIViewPropertyAnimator類實現了需要的所有基礎功能,使得難度降低了許多。在 session 的現場演示中,工程師演示了使用該類從頭打造可全程在非交互與交互狀態之間自由切換的轉場動畫。轉場協議為了實現高度定制化,定義的方法是比較冗余的,iOS 10 在此基礎上引入的新 API 使得協議更加復雜,雖然在演示中添加的代碼只有百來行,另一方面演示的轉場動畫本身也相對復雜,使得這一切看上去很非常復雜。
事實上,依靠UIViewPropertyAnimator類,在實現轉場動畫在非交互與交互狀態之間自由切換的基礎上,還可以大幅精簡現有的轉場協議體系。但轉場動畫本身是個很繁雜的話題,展開講將占用大量的篇幅,這部分具體內容我放在了「iOS 視圖控制器轉場詳解」更新的章節里。轉場動畫本質上是相關視圖控制器的轉換,并將其中視圖的轉換使用動畫的形式展現。除去控制器的部分,轉場動畫就與使用 UIView 下面這個方法來實現的的視圖轉換動畫無異。
transitionFromView:toView:duration:options:completion:
objc.io 在「交互式動畫」中探討了如何讓普通的動畫實現交互,這與 iOS 10 對轉場動畫的改進是一脈相承的,因此接下來我將使用UIViewPropertyAnimator類來繼續 objc.io 的探討來深度講解新 API。
新 API 實踐
要實現的效果如下:
這個簡單的位移動畫里包含了兩套交互:滑動控制(pan 手勢)和點擊控制(tap 手勢),要解決三個轉換問題,也是所有交互動畫需要解決的問題:
Animation to Gesture:動畫過程中切入滑動控制,需要中止當前的動畫并由手指來控制控制板的移動;
Gesture to Animation:滑動結束后添加新的動畫,并與當前的狀態平滑銜接,這需要 Spring 動畫;
Animation to Animation:動畫過程中每次點擊視圖后使動畫逆轉。
前面提到UIViewPropertyAnimator封裝了交互動畫需要的所有基礎功能,實現交互動畫的難度大大降低了,這篇文章似乎沒有寫的必要了。以上每個轉換問題該類都有幾種解決辦法,使用方法非常靈活,但相對地,復雜性增加了不少,也有不少地方需要注意。這次不像上篇中分別解決三個轉換問題,而是將之歸類為實現滑動控制和點擊控制,并首先解決后者。
點擊交互:逆轉動畫
先進行設置:
添加的 Animation Block 和 Completion Blcok 是一次性的,不會重復使用。接下來處理 Tap 手勢:
上面的代碼逆轉動畫的效果如同下面的 BeginFromCurrentState,而我們更需要的是更加自然的 Additive 效果,雖然在這個場景里,0.5s的動畫時間無法看出這兩種效果的差別:
實現 Additive 效果可以通過添加反向的動畫來實現,使用 UIView Animation 時也是這樣做來逆轉動畫:
//每次 Tap 手勢結束后添加向反方向運動的動畫animator.addAnimations({//targetY 為相反位置的坐標panelView.center.y = targetY })
為何不選擇這種方法?不能僅僅為了展示UIViewPropertyAnimator不同于 UIView Animation 的特性而讓效果打折,事實上,這是無奈之舉:不知是否是 Bug,當 Spring Timing 的初始速度不為(0, 0)時,這種方法無法實現 Additive 效果,而是中止動畫直接跳躍到最終位置,其他類型的 Timing 則沒有這個問題,然而這個場景里的位移動畫必須是帶初始速度的 Spring 動畫;不過即使此處不要求初始速度>0,通過添加反向動畫實現 Additive 效果的做法也會有瑕疵,同樣不知是否 Bug:最初添加的動畫的運行時間截止時,如果依然添加動畫,動畫會直接跳躍到最終位置。
其實UIViewPropertyAnimator使用初始速度不為(0, 0)的 Spring Timing 也可以實現 Additive 效果,關鍵在于isInterruptible屬性,默認為 true。禁用這個屬性后,UIViewPropertyAnimator完全與 UIView Animation 無異,上段里提到的問題都不存在;然而,禁用這個屬性后,UIViewAnimating協議里定義的與交互動畫有關的方法和屬性都不能使用:包括上面使用的暫停和逆轉動畫的功能,以及接下來會用到的停止動畫的功能,禁用后使用這些方法和屬性會觸發異常。將UIViewPropertyAnimator當作 UIView Animation 使用的話,去看上篇就好了,我在文末給出的 Demo 里示范了這種用法。
綜合來講,UIViewPropertyAnimator逆轉轉動畫的效果比不上 UIView Animation ,現在暫且帶著效果打折的遺憾繼續使用UIViewPropertyAnimator來實現滑動交互。
滑動交互:控制進度、平滑轉變
當手指接觸到視圖時,如何中止當前的動畫?UIViewPropertyAnimator給了我們兩個選擇:暫停或停止動畫。在使用 UIView Animation 時,我們直接取消了視圖的動畫,也就是停止動畫,這里選擇用該類的方式來停止動畫:
停止動畫還有另外一種使用方法:
不管手指接觸控制板視圖時是否在運動中,手指離開屏幕后都需要添加新的彈簧動畫。然而上面的方案在特定條件下有漏洞:假設此時控制板處于打開狀態(底部位置),用戶向上滑動來關閉控制板,滑動結束后控制板在動畫中移往頂部位置,如果用戶想取消這個操作,于是點擊了控制板視圖,那么控制板視圖最終并不會回到底部位置,而是在中間某個位置(滑動結束時的位置)。造成這個結果的根源在于點擊交互的實現手法:如果是通過添加反向的動畫來實現逆轉,那么就不會出現這個問題;而無論是出于展示新 API 特點的目的還是為了能夠在這里使用stopAnimation:方法,我選擇了使用isReversed屬性來逆轉動畫。滑動結束后動畫的起始位置是手指離開屏幕的位置,使用isReversed逆轉動畫最終只能回到這個位置,而這個位置肯定和控制板在打開/關閉狀態所處的位置有段差距。
選擇使用isReversed來逆轉動畫時,在所有連續類型的手勢參與的交互動畫里,使用stopAnimation:都會有這樣的漏洞。完美的解決方案是在手指接觸視圖時將其暫停,不過不注意的話也會出現這樣的漏洞:
使用pauseAnimation()能夠解決這個漏洞的原因在于:在手勢的起始階段為控制板視圖提供從底部位置到頂部位置的完整動畫,逆轉后始終能夠回到正確的位置;而使用stopAnimation:時不能提供完整路徑的動畫。
如果不在手勢的起始階段就添加動畫,而是在手勢的結束階段才添加動畫,pauseAnimation()也會出現上述漏洞;另一方面,使用stopAnimation:無法在手勢的變化階段控制動畫的進度,只能修改視圖本身。從這兩點考慮,實現轉場動畫以及在非交互與交互狀態之間自由切換應該選擇pauseAnimation()這條路線。
continueAnimation(withTimingParameters:durationFactor:)是UIViewImplicitlyAnimating協議定義的方法,這是保證交互動畫流暢的關鍵,如同使用 UIView Animation 實現交互動畫時 Spring Animation 的作用一樣。這個方法將動畫的起始位置重置為當前位置,然后繼續執行,在這里可以動態修改剩余這段動畫運行時的 Timing 和 Duration。withTimingParameters = nil時,以原來的 Timing 運行,這里以springTiming繼續剩下的動畫;動畫的剩余運行時間為durationFactor * duration,durationFactor = 0時,運行時間依然為原來的duration。因此,
animator.continueAnimation(withTimingParameters:nil, durationFactor: 0)
相當于執行animator.startAnimation()來繼續動畫。
continueAnimation(withTimingParameters:durationFactor:)結束后,animator 的 Timing 依然是初始化時的 Timing,修改只是暫時的;不過durationFactor會修改 animator 原來的的duration(規則未知,每次調用這個方法都會修改,durationFactor = 0不會修改),從而影響后面添加的動畫的運行時間,這是個奇怪的設計。
小結
上面的演示主要偏向于突出UIViewPropertyAnimator在交互方面的特性,它也完全可以當作 UIView Animation 一樣使用,也可以混合這兩種風格,我在ControlPanelAnimation中演示了多種風格實現上面的交互動畫。不過即使假設實現逆轉動畫時的各種瑕疵是實現上的 Bug,在讓普通的動畫實現交互時,UIViewPropertyAnimator相對于 UIView Animation 并不具備優勢:相比上篇中使用 UIView Animation 時的簡單,UIViewPropertyAnimator引入的交互狀態和解決不同轉換問題時看似靈活的搭配選擇,都顯得太復雜了。
不過,使用UIViewPropertyAnimator實現轉場動畫在非交互與交互狀態之間的自由切換是非常方便的,而且還能大幅精簡當前復雜的轉場協議體系,這得益于其封裝的交互功能解決了最困難的部分,具體可查看「iOS 視圖控制器轉場詳解」。
參考
WWDC 2016 Session 216: Advances in UIKit Animations and Transitions:https://developer.apple.com/videos/play/wwdc2016/216/
iOS 視圖控制器轉場詳解:https://github.com/seedante/iOS-Note/wiki/ViewController-Transition
感謝徐川對本文的審校。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微信號:InfoQChina)關注我們。