為了有彈性地開發動畫,Flutter抽象出Curve、Tween、Animation、Tick等概念,然而,直接面對這些元件時,仍需處理許多細節。實務上,我們可以從內建的隱式動畫、顯式動畫元件來著手,必要時再逐步深入。

隱式動畫元件

若只想讓Widget元件在尺寸、位置、透明度等方面,能展現出一些動畫效果,我們可以使用既有的隱式動畫元件(Implicitly animated widget),之所以稱其為隱式,是因為不用接觸Tween、Animation、Tick等細節。舉例來說,若有個Container,會依zoom_in來決定寬度,以進行縮放:

Container(
child: Image.asset('images/caterpillar.png')
width: zoom_in ? 100 : 300)

現在想進一步讓縮放過程具有動畫效果的話,只要使用對應的AnimatedContainer,指定變化過程的持續時間就可以了,例如,指定動畫期間為一秒:

AnimatedContainer(
child: Image.asset('images/caterpillar.png'),
width: zoom_in ? 100 : 300,
duration: Duration(seconds: 1))

對於100與300間的變化值,AnimatedContainer預設用線性內插來計算,這可由curve特性控制,預設值是Curves.linear,在〈Curves〉的API文件,可看到各種緩動曲線(Easing curve),其中也有標示各曲線大致的效果展示,必要時也可以繼承Curve,重新定義transform方法來實現自己的緩動曲線。

在〈ImplicitlyAnimatedWidget〉API文件,我們可以看到與非動畫元件相對應的隱式動畫元件,而這些元件本質上是作為一個包裹器,藉由控制包裹器的特性,來實現對子元件的動畫。

我們若想操作某元件的特性(如直接設定Image的width)來完成動畫,可用TweenAnimationBuilder。Tween的意思是between,顧名思義可指定特性的起始(begin)與終值(end),緩動曲線計算後的值傳給Tween,Tween依超始與終值計算後,再傳給TweenAnimationBuilder的builder函式,這時可用來設定元件特性,實際上AnimatedContainer就是檢查自身特性變化,作為起始與終值來完成動畫。

顯式動畫元件

隱式動畫元件的效果是單向的,想建立往復式的動畫呢?像是自動重複放大縮小?我們可透過顯式動畫元件(Explicitly animated widget),稱為顯式,因為我們須親自控制AnimationController,如呼叫forward、reverse、repeat、stop等方法來控制動畫。

具體來說,顯式動畫元件是〈AnimatedWidget〉的子類,在API文件,我們可以看到內建的隱式動畫元件,都需要接受Animation<double>的特性,但會依動畫效果而有不同的命名,例如,ScaleTransition的scale特性,或者是SlideTransition的position特性,AnimationController實作了Animation<double>,可以直接指定給這類特性。

類似地,若內建的顯式動畫元件無法滿足,我們也可以使用AnimatedBuilder,例如,想直接操作某元件的特性,並進行往復式的動畫,此時可採用這種作法。如果想指定緩動曲線或是起始與終值呢?

緩動曲線的動畫控制,可由CurvedAnimation來處理,它實作了Animation<double>,而Tween有個animate方法,可以接受Animation<double>,傳回Animation<double>,這就形成有趣的模式。Animation<double>可以套接,最基本的就是緩動曲線與Tween的套接,例如:

Animation<double> ani = AnimationController(...)..repeat();
ani = CurvedAnimation(parent: ani, curve: Curves.easeOut);
ani = ColorTween(begin: Colors.white, end: Colors.red).animate(ani);

在動畫開始後,ani的value就會是套用了緩動曲線與Tween的結果;Animation<double>的套接還,可以用來組合更複雜的交錯動畫(Staggered animation),也就是可以編輯時間軸的不同曲區,指定不同的動畫效果,這部份,開發者可以指定CurvedAnimation的curve為Interval來達成。

Interval也是Curve的子類,在緩動曲線設計上,時間會標準化為0.0到1.0,Interval可指定這個區間內,有哪個範圍要套用曲線,例如被指定為Interval(0, 0.25)時,只有在0到0.25才會套用指定的緩動曲線。

深入動畫細節

運用顯式動畫元件時,須建立AnimationController以控制動畫,此時必須指定vsync。如果不關心動畫底層細節,仍可看到文件上都會提到:自訂Widget時with一下SingleTickerProviderStateMixin,再將this指定給vsync就可以了。然而vsync到底是什麼?

想知道答案的話,就必須知道Flutter如何完成動畫,令人驚奇地,Flutter是透過不斷地重建Widget樹來完成動畫,每次的重建結果就是一個影格(frame),在理想情况下,Flutter能達到60FPS(24FPS就滿順暢了,32FPS以上,人眼感覺不到差異),每次重建的特性值就是透過Animation<double>的value,Animation<double>可以套接,最初的value來源就是AnimationController。

那麼,AnimationController是怎麼變化其value?透過Ticker!也就是計時器,建立Ticker時,須指定一個回呼函式,該函式會在每次計時時被呼叫,AnimationController的value就是回呼函式中被改變的,Ticker是個需要管理的資源,像是建立、啟動、停止等,TickerProvider的實現就是用來管理Ticker。

而且,AnimationController的vsync型態是TickerProvider,SingleTickerProviderStateMixin實作TickerProvider,Single的意思是它的createTicker方法只能被呼叫一次,之後只用這唯一的Ticker來計時。

而每次重建Widget樹時,是由誰控制的?AnimationController!它可透過addListener指定傾聽器函式,每次value變化時會呼叫該函式,Flutter內建的動畫元件,就是在value變化時,呼叫setState、通知Flutter狀態改變了,從而重建Widget樹。

AnimationController內部只會使用一個Ticker,通常開發者只需一個AnimationController,因此用SingleTickerProviderStateMixin即可,若要操作多個AnimationController做複雜的畫,可用TickerProviderStateMixin,createTicker能多次呼叫,TickerProviderStateMixin會用Set管理建立的Ticker。

路由動畫控制

我在先前專欄〈Flutter導覽/路由的是與非〉談過,代表某個資源的銜接,資源包含了最後的頁面,以及中間的動畫,Flutter預設會使用平台原生的動畫效果來切換頁面,若想自定頁面切換效果時,可以透過PageRouteBuilder,其pageBuilder用來組建要銜接的頁面,transitionsBuilder指定動畫的效果。

無論是pageBuilder或transitionsBuilder,第二個參數的型態都是Animation<double>,換言之,PageRouteBuilder會幫你管理Animation<double>等資源,你只要決定Animation<double>是否進一步套接,或者指定給顯式動畫元件的對應特性即可。

若兩個路由銜接的頁面有著相同的元件(例如同一張圖片),在頁面切換時想要令圖片無縫地轉移,Flutter提供了有趣的Hero元件,只要兩個頁面都用Hero來包裹相同的元件,並設定相同的tag值,圖片就會自然地過渡至下一個頁面。

簡單來說,在Flutter使用動畫有多種方式,我們可用隱式動畫、TweenAnimationBuilder、顯式動畫、AnimatedBuilder的順序思考,必要時結合繪圖,不致於初期面對Curve、Tween、Animation的複雜性。

專欄作者

熱門新聞

Advertisement