
在 Flutter 中,動畫在大部分開發情境下不太常使用到,很多產品都以功能為導向,有時候有趣的體驗或是酷炫的效果都會被忽略甚至是排在後面,視為往後優化的部分,所以大部分實際遇到需要動畫的情況也不多,就我了解,大部分開發者除了自身興趣外應該都對它沒有很熟悉。所以藉這個機會跟大家分享一些撰寫動畫的重點,也包含一些實作經驗與範例,希望能讓你更熟悉它。
主要動畫的分類可以分成兩種來識別,一種是 Explicit Animation 顯式動畫和 Implicit Animation 隱式動畫,在不一樣的情境下有不一樣的選擇,實現的方式有很多種,如何高效的去使用才是重點。這時侯我想大家看這兩個分類應該還是很難懂我在說什麼,沒關係,跟著我繼續往下吧!
AnimationController,透過它我們才能完全的控制動畫,包括設置運行時間長度,會用到 Duration、開始動畫能設置初始點、反轉動畫、停止動畫等等,搭配 Tween 補間差值,能做到任何的效果呈現AnimationController 都會產生一個對應的數值,讓元件根據數值進行顯示上的變化。而當不使用 Tween 時(後面會提到),就是線性的依次產生一個 0-1 的數值AnimatedBuilder 來進行元件的更新,務必使用它包裹 Widget Tree,才能無縫的在每幀進行刷新。切記不要用 setState(),雖然一樣能完成動畫效果,但實際上它無法渲染訊號同步,一旦有多個動畫要執行,或是牽扯到的範圍很大,很可能會造成卡頓AnimatedWidget 或 AnimatedBuilder 實作動畫效果。我們可以自定義某個複用的效果元件,並以 XxxTransition 此規則來命名,必備參數為 AnimationController,通常只要看到 Transition 為後綴的元件都是顯示動畫,算是大家的共識begin 和 end 可以設置可以根據特性來判斷是否使用它來實作
補充:實作時可以透過 child 參數設置不被動畫影響的元件,避免重複創建、提高效能,而最好的方式還是用
constWidget,節省記憶體使用
| 元件 | 說明 | 
|---|---|
| AlignTransition | 對齊動畫 | 
| DecoratedBox | 裝飾動畫 | 
| DefaultTextStyle | 文字風格動畫 | 
| Fade | 淡入淡出動畫 | 
| Positioned | 位置動畫 | 
| RelativePositioned | 相對位置動畫 | 
| Rotation | 旋轉動畫 | 
| Scale | 大小倍率動畫 | 
| Size | 尺寸動畫 | 
| Slide | 滑動動畫 | 
| StatusTransitionWidget | 狀態改變元件 | 
AnimationController,相對簡單許多,使用起來很快速、便利,只需要運行的時間長度 Duration,然後設定改變的目標值,它就能幫你做完所有事情。不過就無法控制動畫| 元件 | 說明 | 
|---|---|
| AnimatedAlign | 對齊動畫 | 
| AnimatedContainer | 綜合動畫,更改支援的所有屬性都會有動畫效果 | 
| AnimatedCrossFade | 針對兩個元件執行交換的 Fade 動畫效果 | 
| AnimatedDefaultTextStyle | 文字動畫 | 
| AnimatedOpacity | 透明度動畫 | 
| AnimatedPhysicalModel | 陰影動畫 | 
| AnimatedTheme | 主題風格動畫 | 
| AnimatedSize | 大小尺寸動畫 | 
| AnimatedPadding | Padding動畫 | 
| AnimatedRotation | 旋轉動畫 | 
| AnimatedSwitcher | 元件漸變動畫,跟 AnimatedCrossFade 類似 | 
| AnimatedScale | 動畫版本的 Transform.scale,影響大小 | 
| AnimatedSlide | 滑動動畫 | 
| AnimatedPositioned | 位置動畫 | 
| AnimatedPositionedDirectional | 位置方向動畫 | 

SingleTickerProviderStateMixin 並在創建 AnimationController 的時候設置 vsync 參數為 this,緊接著就能開始製作動畫了SingleTickerProviderStateMixin,顧名思義它就是適合一個 AnimationController 的情境。如果需要多個 AnimationController 來管理多個動畫,可以選用 TickerProviderStateMixin,同時管理多個 Ticker 實體與每幀更新同步詳細動畫的刷新過程跟源碼分析可以閱讀下一篇文章,分享了細節,這邊就不深入探討了。
Day 12: 研究 Flutter 動畫,背後的 vsync 跟 Ticker 有多重要?
vsync 創建一個 TickerProviderTween<T>,使用泛型放置你期望的型別,當然也可以使用特定類型的 Tween 類去替代,下方有幫大家條列了animate() 生成 Animation 物件| 類型 | 說明 | 
|---|---|
| IntTween | 數值變化 | 
| StepTween | 使用 double 刪除小數值返回整數部分 | 
| ColorTween | 顏色變化 | 
| SizeTween | 大小變化 | 
| BoxConstraintsTween | 約束變化 | 
| DecorationTween | 裝飾變化,例如:BoxDecoration、ShapeDecoration | 
| EdgeInsetsTween | EdgeInsets變化,可搭配 Padding 使用 | 
| Matrix4Tween | 矩陣變化 | 
| TextStyleTween | 文字風格變化 | 
| FractionalOffsetTween | 小數變化 | 
| MaterialPointArcTween | 圓弧變化 | 
| RectTween | 矩形變化,使用 null 代表 Rect.zero | 
| AlignmentTween | 對齊變化 | 
| ConstantTween | 常數變化 | 
實現動畫的核心類,根據 Tween 生成更新的區間數值,而元件根據數值的更新來重繪,產生動畫效果
// 1.
Animation animation = _animationController.drive(
  Tween<Offset>(
    begin: const Offset(0, 0),
    end: const Offset(100, 200),
  ),
);
// 2.
Animation animation = Tween<Offset>(
  begin: const Offset(0, 0),
  end: const Offset(100, 200),
).animate(_animationController);
Animation animation = Tween(
      begin: 0,
      end: 50,
    )
        .chain(
          CurveTween(curve: Curves.easeIn),
        )
        .animate(animation);
Tween 自定義特殊情境的差值,任何類型的改變,都可以作為 Tween
t 進行處理和計算,讓結果不同以下範例,實作出文字陸續出現的效果,就像打字機一樣:
class TypingTween extends Tween<String> {
  TypingTween({
    String begin = '',
    String? end,
  }) : super(
          begin: begin,
          end: end,
        );
  @override
  String lerp(double t) {
  	final endStringLength = end?.length ?? 0;
    final cutPosition = (endStringLength * t).round();
    final displayedText = end?.substring(0, cutPosition) ?? '';
    return displayedText;
  }
}

Curves 提供了很多不同類型的選擇,覆蓋了大部分的使用場景,例如:Curves.easeIn、Curves.bounceInOut、Curves.fastOutSlowIn,總共 38 種,詳細可以到官方文件上了解Curves.easeIn → 動畫從慢速開始然後加速Curves.easeInOut → 動畫從慢速開始,加速,然後減速
… 詳細可查看官方文件,有呈現所有的運動效果
Interval 實作。第一個參數為開始,第二個參數為結束,設定 0-1,例如:可以設定時間長度在 0.25 開始執行動畫Animation animation = Tween<Offset>(begin: const Offset(0, 0), end: const Offset(100, 200)).animate(_animationController);
// 1.
animation = CurvedAnimation(parent: _animationController, curve: Cureves.easeInOut)
// 2.
Animation animation2 = CurvedAnimation(
  parent: _animationController,
  curve: const Interval(0, .6, curve: Curves.fastOutSlowIn),
);
AnimationController 搭配使用,精準的進行畫面重繪child → 設置不需要更新、變化的元件,不會因為動畫執行而重複創建和浪費資源,更好的是幫元件加上 const,確保編譯時就創建確定builder(context, child) → 可以直接拿 child 來用,它就是我們賦予不會被影響的部分,外面包裹需要動畫更新的元件再提醒一次,切記盡量不要使用
addListener()和setState()進行動畫刷新,尤其是擁有一個 Long widget tree,會降低 APP 性能


build() 的 Widget Tree 變得腫大且難閱讀時,可以將動畫部分獨立出來一個新的 Widget。這時候很適合使用自定義的 AnimatedWidget,將 AnimatedBuilder 包成 Widget,除了可讀性高之外,以後也可以複用,不需要重寫相同效果,可以實作一個自己的動畫元件集合Listenable 作為參數,AnimationController、Animation 都是它的子類,根據命名規則,通常會以 xxxTransition 的命名方式child 參數作為性能優化,可以提前創建不被影響
static final 聲明,節省記憶體消耗TweenAnimationBuilder(
  tween: ColorTween(begin: Colors.blue, end: Colors.green),
  duration: const Duration(milliseconds: 1500),
  curve: Curves.bounceInOut,
  builder: (context, tween, child) {
    return Container(
      width: 100.0,
      height: 100.0,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: tween,
        borderRadius: BorderRadius.circular(20.0),
      ),
      child: child,
    );
  },
)

以下是在實際開發場景中,我們如何針對動畫的需求條件來決定要使用哪種方式來實現。(此圖參考 Emily Fortura 製作中文版本)
sin(value),數值越長頻率越高,越小波形越平緩
clock 套件,透過模擬的時間,在測試環節可快速跳過並驗證。不需要使用 Future 和 Datetime 耗費真實時間本文說明了動畫的核心幾部分,希望有讓大家了解在什麼情境下要選擇什麼實作方式,通常一種動畫效果可以有很多種方式來完成它,但我們可以挑相對快速且方便的作法,根據動畫的作動、行為、可操作性來判斷。如果都不夠你用的話,那我們就使用 Canvas 自己畫吧,有興趣的朋友跟我說,會在出其他文章來討論。
動畫除了是一個效果、一個產品需求之外,它同時也是提升使用者體驗的重要元素,當市面上產品的呈現方式都差不多時,可以想想是否能讓自家產品脫穎而出,但凡事過多都會造成反效果,所以規劃、嘗試很重要,適當才能夠畫龍點睛。思考一下,讓 APP 擁有自己的特點吧!
Day 12: 研究 Flutter 動畫,背後的 vsync 跟 Ticker 有多重要?