【Flutter 專題】71 圖解基本隱式動畫 Widget|8月更文挑戰

阿策小和尚發表於2021-08-16

      “這是我參與8月更文挑戰的第17天,活動詳情檢視: 8月更文挑戰” juejin.cn/post/698796…

      小菜前段時間自定義 ACEStepper 步進器時,在 ACEStep 中嘗試過 AnimatedCrossFade 用於在兩個 Widget 切換過度,簡單實用,今天小菜重點學習一下並嘗試相關隱式動畫 Widget

AnimatedCrossFade 淡入淡出動畫

原始碼分析

const AnimatedCrossFade({
    Key key,
    @required this.firstChild,                  // 首個展示 Widget
    @required this.secondChild,                 // 第二展示 Widget
    this.firstCurve = Curves.linear,            // 首個 Widget 展示動畫
    this.secondCurve = Curves.linear,           // 第二 Widget 展示動畫
    this.sizeCurve = Curves.linear,             // 切換時尺寸動畫
    this.alignment = Alignment.topCenter,       // 對齊方式
    @required this.crossFadeState,              // 切換狀態(是否切換)
    @required this.duration,                    // 切換動畫時長
    this.reverseDuration,                       // 切換反向動畫時長
    this.layoutBuilder = defaultLayoutBuilder,  // Widget 佈局構造器
})
複製程式碼

      分析原始碼可知,AnimatedCrossFade 可以在指定時間內從一個 Widget 到另一個 Widget 的平滑過渡或反向過渡;其中切換狀態和時長是必要屬性;

案例嘗試

  1. 小菜嘗試一個基本的動畫過程,兩個方塊之間進行切換;
return GestureDetector(
    onTap: () { setState(() => isChanged = !isChanged); },
    child: Container(
        child: AnimatedCrossFade(
            firstChild: Container(width: 100, height: 100, color: Colors.purpleAccent.withOpacity(0.4)),
            secondChild: Container(width: 200, height: 200, color: Colors.blueGrey.withOpacity(0.4)),
            duration: Duration(milliseconds: 1500),
            crossFadeState: isChanged ? CrossFadeState.showSecond : CrossFadeState.showFirst)));
複製程式碼

  1. reverseDuration 為切換反向動畫時長;
reverseDuration: Duration(milliseconds: 500),
複製程式碼

  1. firstCurve / secondCurve 為兩個 Widget 切換時動畫效果;動畫效果有多種,小菜不在此贅述;
firstCurve: Curves.fastOutSlowIn,
secondCurve: Curves.easeInExpo,
複製程式碼

  1. alignment 為尺寸動畫切換時對齊位置,當兩個 Widget 大小不同時效果明顯,小菜嘗試了兩種位置進行對比;
alignment: Alignment.bottomRight,

alignment: Alignment.center,
複製程式碼

  1. sizeCurve 為尺寸切換動畫,當兩個 Widget 大小不同時效果明顯;
sizeCurve: Curves.easeInExpo,

sizeCurve: Curves.fastOutSlowIn,
複製程式碼

  1. layoutBuilder 為佈局構造器,這個是小菜認為最值得研究的地方,構造器並不陌生,但在這裡的作用卻比較特殊,通過 Stack 將兩個 Widget 層級疊放,底部 Widget 預設尺寸位置以上層 Widget 為基準,預設 Position 邊距均為 0.0;我們可以自定義調整動畫起始位置;
// 預設
static Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
  return Stack(
      overflow: Overflow.visible,
      children: <Widget>[
        Positioned(key: bottomChildKey, left: 0.0, top: 0.0, right: 0.0, child: bottomChild),
        Positioned(key: topChildKey, child: topChild)
      ]);
}
// 調整 Position 位置
layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
  return Stack(children: <Widget>[
    Positioned(key: bottomChildKey, left: 50.0, top: 50.0, right: 50.0, bottom: 50.0, child: bottomChild),
    Positioned(key: topChildKey, child: topChild)
  ]);
}
複製程式碼

      AnimatedCrossFade 原始碼

AnimatedSwitcher 切換動畫

原始碼分析

const AnimatedSwitcher({
    Key key,
    this.child,
    @required this.duration,                // 切換動畫時長
    this.reverseDuration,                   // 反向切換動畫時長
    this.switchInCurve = Curves.linear,     // 切換顯示時動畫曲線
    this.switchOutCurve = Curves.linear,    // 切換隱藏時動畫曲線
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,  // Widget 動畫構造器
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,          // Widget 佈局構造器
})
複製程式碼

      分析原始碼可知,AnimatedSwitcher 更加靈活,可自由設定切換動畫之間顯示隱藏動畫效果;當 child Widget 內容或 Key 有變更時,old child 會執行隱藏動畫,new child 會執行展現動畫;

案例嘗試

  1. 小菜嘗試切換兩個基本的方塊,但剛開始切換動畫時長和反向切換動畫時長沒有效果,兩個 Widget 只有引數更新,動畫效果未執行;小菜嘗試加入 Key 區分之後正常;
return GestureDetector(
    onTap: () => setState(() => isChanged = !isChanged),
    child: AnimatedSwitcher(
        duration: Duration(milliseconds: 500),
        reverseDuration: Duration(milliseconds: 1500),
        child: isChanged
            ? Container(key: UniqueKey(), color: Colors.purpleAccent.withOpacity(0.4),  width: 100, height: 100)
            : Container(key: UniqueKey(), color: Colors.green.withOpacity(0.4), width: 150, height: 120)));
複製程式碼

  1. 小菜在切換過程中嘗試不同的顯示隱藏動畫效果;
switchInCurve: Curves.easeInCubic,
switchOutCurve: Curves.fastLinearToSlowEaseIn,

switchInCurve: Curves.easeInExpo,
switchOutCurve: Curves.fastOutSlowIn,
複製程式碼

  1. transitionBuilder 為動畫構造器,可以自定義動畫效果;小菜嘗試了兩種簡單的縮放動畫和平移動畫,暫未嘗試複雜動畫;且動畫屬性與顯示隱藏的 switchInCurve / switchOutCurve 動畫曲線共同展示效果;
// 縮放動畫效果
return GestureDetector(
    onTap: () => setState(() => isChanged = !isChanged),
    child: AnimatedSwitcher(
        duration: Duration(milliseconds: 500),
        reverseDuration: Duration(milliseconds: 1500),
        switchInCurve: Curves.easeInCubic,
        switchOutCurve: Curves.fastLinearToSlowEaseIn,
        child: isChanged
            ? Container(key: UniqueKey(), color: Colors.purpleAccent.withOpacity(0.4), width: 100, height: 100)
            : Container(key: UniqueKey(), color: Colors.green.withOpacity(0.4), width: 150, height: 120),
        transitionBuilder: (Widget child, Animation<double> animation) {
          return ScaleTransition(scale: animation, child: child);
        }));
        
// 平移動畫效果
return GestureDetector(
    onTap: () => setState(() => isChanged = !isChanged),
    child: AnimatedSwitcher(
        duration: Duration(milliseconds: 500),
        reverseDuration: Duration(milliseconds: 1500),
        switchInCurve: Curves.easeInExpo,
        switchOutCurve: Curves.fastOutSlowIn,
        child: isChanged
            ? Container(key: UniqueKey(), color: Colors.purpleAccent.withOpacity(0.4), width: 100, height: 100)
            : Container(key: UniqueKey(), color: Colors.green.withOpacity(0.4), width: 150, height: 120),
        transitionBuilder: (Widget child, Animation<double> animation) {
          return SlideTransition(child: child, position: Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0)).animate(animation));
        }));
複製程式碼

  1. childold/new Widget 一般是以 Stack 層級儲存,在動畫過程中兩個 Widget 均要展示,可以通過 layoutBuilder 佈局構造器進行自定義;小菜嘗試調整對齊方式和只展示 current Widget 動畫效果;
// 調整層級對齊方式
layoutBuilder: (Widget currentChild, List<Widget> previousChildren) {
  return Stack(children: <Widget>[
    ...previousChildren,
    if (currentChild != null) currentChild
  ], alignment: Alignment.bottomRight);
}
            
// 只展示當前 Widget 動畫效果
layoutBuilder: (Widget currentChild, List<Widget> previousChildren) {
    return currentChild;
}
複製程式碼

  1. AnimatedSwitcher 可以設定多個 Widget 平滑切換,相對於 AnimatedCrossFade 可擴充套件性更高;小菜嘗試三個 Widget 平移切換;
return GestureDetector(
    onTap: () => setState(() {
          ++index;
          index = index % 3;
        }),
    child: AnimatedSwitcher(
        duration: Duration(milliseconds: 500),
        child: _animatedItemWid(index),
        transitionBuilder: (Widget child, Animation<double> animation) {
          return SlideTransition(child: child, position: Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0)).animate(animation));
        }));

Widget _animatedItemWid(index) {
  switch (index) {
    case 0:
      return Container(key: UniqueKey(), color: Colors.purpleAccent.withOpacity(0.4), width: 100, height: 100);
      break;
    case 1:
      return Container(key: UniqueKey(), color: Colors.green.withOpacity(0.4), width: 150, height: 120);
      break;
    case 2:
      return Container(key: UniqueKey(), color: Colors.orange.withOpacity(0.4), width: 120, height: 140);
      break;
  }
}
複製程式碼

      AnimatedSwitcher 原始碼


      Flutter 還提供了很多靈活的隱式動畫 Widget,小菜認為這兩類最靈活,使用場景最多;小菜對隱式動畫研究還不夠深入,如有錯誤請多多指導!

來源: 阿策小和尚

相關文章