iOS交互式動畫詳解(上):iOS 10以下的實現

不久前結束的 WWDC 2016 Session 216: Advances in UIKit Animations and Transitions 介紹了 iOS 10 的新動畫 API,讓動畫與交互無縫連接,這是「開發者的大事、大快所有人心的大好事」。兩年前 objc.io 在「交互式動畫」一文在探討了這個話題,本文先來探討 iOS 10 以下的系統對交互動畫的支持,在下篇中深度解讀 iOS 10 新 API。

交互動畫類型

其實交互式動畫在 iOS 系統裏可以說是司空見慣的。在可交互動畫的執行過程中交互手段(一切控制當前動畫的手段,主要是手勢)會隨時切入動畫過程,根據交互結束後是否更改了動畫流程可以將交互動畫分爲兩種:一種會更改動畫流程,比如 UIScrollView 的滑動動畫,如今看來很普通,在 iPhone 問世之初這個效果可是征服人們的一大利器,「喬布斯在第一次展示 iPhone 時,他特別指出當他給別人看了這個滑動例子,別人說的一句話: 當這個界面滑動的時候我就已經被征服了。」(出自「交互式動畫」一文),在這個滑動動畫裏每次手指在界面上滑動時,前一個滑動動畫被中止,當手指離開屏幕後,添加一個新的滑動動畫;另一種僅僅控制動畫進度而不修改動畫,典型代表是交互轉場動畫,除了帶來便利的操作,驚豔的轉場動畫也是個有力的視覺征服利器。

這兩種交互動畫的實現手法是不一樣的。後一種涉及暫停、恢復和逆轉動畫,在系統支持的交互轉場裏,只需要提供一個UIPercentDrivenInteractiveTransition實例並在交互過程中使用updateInteractiveTransition:來更新進度即可,完全不用我們操心其他事情,實現非常簡單。如何在普通的動畫上實現這種控制呢?可以參考我之前發表的「iOS 視圖控制器轉場詳解」中的「自定義容器控制器轉場」章節:暫停和恢復動畫採用官方提供的方法:How to pause the animation of a layer tree?;手動控制動畫進度則需要在暫停動畫的基礎上更新 CAMediaTiming 協議(CALayer 遵守該協議)中的timeOffset屬性;而在交互結束後逆轉動畫則需要CADisplayLink的幫助。iOS 10 引入的新 API 對這些操作進行了封裝,實現會簡單得多,同時兼容了前一種交互動畫的實現方法,打破了兩種交互動畫的界限。

相關廠商內容

通過探針技術,實現Java應用程序自我防護

新Java,新未來

你離成爲一位合格的技術領導者還有多遠?

你瞭解技術領導與技術管理的差別嗎?

全新華爲企業雲專區現已正式上線!

相關贊助商

QConSHlogo.jpg

QCon全球軟件開發大會上海站,2016年10月20日-22日,上海寶華萬豪酒店,精彩內容搶先看

objc.io 在「交互式動畫」一文中探索了前一種交互式動畫,實現了下面這種類似控制中心的效果:

000.gif

這個簡單的位移動畫裏包含了兩套交互:滑動控制(pan 手勢)和點擊控制(tap 手勢),要解決三個轉換問題,也是所有交互動畫需要解決的問題:

  1. Animation to Gesture:動畫過程中切入滑動控制,需要中止當前的動畫並由手指來控制控制板的移動;

  2. Gesture to Animation:滑動結束後添加新的動畫,並與當前的狀態平滑銜接;

  3. Animation to Animation:動畫過程中每次點擊視圖後使動畫逆轉。

objc.io 的兩位作者使用了三種方法來實現這個交互動畫,手法都是實現彈簧動畫(Spring Animation)去驅動控制板視圖的移動:

  1. 基於 UIKit Dynamics 框架,這是 iOS 7 引入的模擬真實物理行爲的動畫框架,對控制板視圖賦予了彈簧的行爲,每次移動都如同有一個彈簧將視圖拉向目標位置;

  2. 自己動手實現彈簧動畫,所謂動畫就是數值的連續變化,作者根據彈簧的胡克定律實現一個算法來計算物體在運動過程中的位置,前面提到的CADisplayLink是個能夠與屏幕刷新頻率同步的定時器,通過調用指定的方法,每次屏幕刷新時更新視圖位置,效果與普通的動畫無異。

  3. 將在2中實現的彈簧動畫使用 Facebook 的 POP 框架驅動。

這三種方法都沒有使用 UIView Animation 和 Core Animation(前者是後者的封裝),這樣實現普通動畫的交互就比較困難,接下來討論如何使用這兩種動畫 API 來實現上面的交互效果。

Animation to Gesture

添加到 CALayer 上的動畫在結束前如果被取消會造成視覺突變,比如在一個右移的動畫結束前取消該動畫就會造成如下所示的跳躍,從中途直接跳到了終點:

001.gif

因此交互動畫首要解決的就是一個很知乎的問題:「如何優雅地中止運行中的動畫而不造成畫面突變?」答案是:取消動畫時讓 modelLayer 的狀態與當前 presentationLayer 的狀態同步。在手勢切入控制板的動畫過程後這樣做:

002.jpg

這裏有個需要注意的地方,如果你使用 UIView Animation,一定要使用帶options的 API,且必須將.AllowUserInteraction作爲選項之一,不然在動畫運行過程中視圖不會響應觸摸事件,使用 Core Animation 則不受此影響。

Gesture to Animation: Spring Animation

上面的目標是:滑動結束後添加新的動畫,並與當前的狀態平滑銜接。這需要手指離開屏幕後添加的新動畫應該以手指離開屏幕時沿 Y 軸的速度開始,否則速度曲線不連續,看着很不自然。離開速度可以從手勢獲取,但是指定動畫的初始速度,在 iOS 7 公開彈簧動畫(Spring Animation)接口之前,現有的動畫 API 裏沒有能夠直接做到這點的,iOS 7 中引入的 UIKit Dynamics 動畫框架也可以實現這個目標,除此之外,要麼像 objc.io 的兩位作者那樣自己動手打造 Spring 效果要麼藉助第三方的動畫庫。

彈簧動畫的 API:

animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:

這個 API 在時間曲線上模擬彈簧的簡諧運動(簡單來講就是來回振盪),實現位移動畫時模擬真實彈簧的行爲。

其中的速率參數initialSpringVelocity是個CGFloat,這顯得很奇怪,爲什麼不是一個向量呢?「交互式動畫」文中對此提出了質疑:「當我們給一個移動 view 的動畫在其運動的方向上加一個初始的速率時,你沒法告知動畫這個 view 現在的運動狀態,比如我們不知道要添加的動畫的方向是不是和原來的 view 的速度方向垂直。爲了使其成爲可能,這個速度需要用向量來表示」。實際上儘管速率參數是個數值而非向量,但彈簧動畫的初始速度是有方向的:不管視圖從(100, 100)移動到(200, 0),還是從(100, 100)移動到(200, 200),初始速度始終是沿着起點到終點的直線方向的。我覺得在這裏這兩位作者陷入了一個誤區,且不說在這個場景裏動畫的方向是明確的(Y 軸,起點和終點我們也知道),他們似乎想用彈簧動畫來實現添加反向的動畫(即視圖在動畫中途返回原點,這是第三個轉換問題),這個質疑的本質是指彈簧動畫無法合成速度,這類似一枚火箭在飛行中啓動引擎在相反方向上添加推動力來減速直至反向運動。但彈簧動畫和其他的動畫 API 都並非由力學引擎驅動,在兩位作者發佈這篇文章的 iOS 7 時期,彈簧動畫是無法做到這點的,從 iOS 8 開始就可以了,但是原因和這個 API 本身沒有關係,下一節來解釋。兩位作者最終放棄了使用這個 API,從而使得整個探索走向了完全不一樣的方向。

另外速率參數如何設置也很令人費解,文檔裏的解釋是這樣的:

A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.

initialSpringVelocity並非直接指定初始速率,動畫初始(變化)速率 = (toValue - fromValue) * initialSpringVelocity,這種相對值的設計避開了動畫的具體變化值,方便使用者估算和設置動畫時間。那麼從(100, 100)移動到(300, 300),如果你希望視圖沿着目標方向的初始速度爲(150, 150),即合成速度約爲150 X 1.4(2的開方值) = 210,直線距離約爲 200 X 1.4 = 280,那麼initialSpringVelocity約爲 210/280 = 0.75。

回到這個階段的問題本身,怎麼解決?

003.jpg

Animation to Animation: Additive Animation

在動畫中途點擊控制板視圖後讓視圖返回到原來的位置,做法是再次添加一個同樣動畫屬性的動畫(使用 Core Animation 時注意使用不同的 key),但在效果上完全抵消,效果有如下幾種:

004.gif

使用 UIView Animation 或者 Core Animation 不做特殊設置的話,效果是第一種;使用 UIView Animation 時指定 BeginFromCurrentState 選項的效果是第二種,位置不會突變但速度有突變;我們需要的是第三種效果,使用 Additive 類型的動畫時,在控制板打開或者關閉過程的任何時刻點擊視圖,視圖將會向反方向移動,動畫不會有位置和速度突變,但 UIView Animation 沒有這個選項。

在 objc.io 的這篇文章發佈後的半個多月正是 WWDC 2014 大會,在 Session 236: Building Interruptible and Responsive Interactions 裏介紹瞭解決上述三個轉換問題的方法,上面的動圖都截取自該 session,前兩個問題的解決辦法就是上面說的那些,也提到了 objc.io 這篇文章裏中使用的 UIKit Dynamics 這個技巧,而最爲棘手的第三個問題需要實現 Additive 類型的動畫,該效果來自 CAAnimation 子類 CAPropertyAnimation 的additive屬性。

005.jpg

additive屬性自 iOS 2 起就存在,文檔解釋:

If YES, the value specified by the animation will be added to the current render tree value of the property to produce the new render tree value. The addition function is type-dependent, e.g. for affine transforms the two matrices are concatenated. The default is NO.

使用 CAKeyframeAnimation 時必須將該屬性指定爲true,否則不會出現期待的結果。不過,在 CABasicAnimation 裏使用這個屬性很需要一番技巧,我在嘗試使用這個屬性時總是得不到想要的效果,直到觀看了這個 session 才恍然大悟,原來是這麼設計的,文檔的解釋是正確的廢話。

如何使用 CABasicAnimation 實現上面的效果呢?非 Additive 的動畫的變化範圍是絕對值設計,添加到 presentationLayer 的動畫的變化範圍是:fromValue -> toValue,Additive 的動畫採用的是相對值設計,添加到 presentationLayer 的動畫的變化範圍是:modelLayerValue + fromValue -> modelLayerValue + toValue。假設控制板開關後的 Y 軸差距爲 500,這樣實現 Additive 效果:

006.jpg

注意指定timingFunction,該值默認爲 nil,效果是線性曲線(Linear),兩個動畫疊加後的效果與 BeginFromCurrentState 等同。但Core Animation 也沒有提供 Spring Timing Function,雖然從 iOS 6 起就有人發現了上面的 CASpringAnimation,但是這個 API 纔到 iOS 9 才公開,而且沒有文檔。而 UIView Animation 沒有提供實現 Additive 效果的選項,只能退而求其次實現 BeginFromCurrentState 的效果。所以點擊後逆轉動畫在 iOS 7 上的效果無法完全滿足設計的要求,可以依靠一些第三方彈簧動畫來彌補,比如 RBBAnimation,基於 CAKeyframeAnimation,支持 iOS 6。

iOS 8 中 UIView Animation 默認實現了 Additive 效果,所以從 iOS 8 開始,解決第三個轉換問題就太容易了,直接添加反向的動畫即可:

007.jpg

小結

從代碼上看,無比簡單。不過別忘了沒有 Additive 類型的動畫,objc.io 在「交互式動畫」中做出的艱辛探索,實現成本要高出許多。在 iOS 7 中利用 UIView Animation/Core Animation 實現交互動畫還有不完美的地方,而 UIKit Dynamics 框架是個非常好的替代選項。從 iOS 8 開始沒有了限制,而 iOS 7 以下的系統則需要自己打造 Spring 動畫或者依靠第三方動畫庫。

我爲這個動畫寫的Demo參見 ControlPanelAnimation :

https://github.com/seedante/ControlPanelAnimation


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章