我什么時候應該使用AnimatedBuilder或AnimatedWidget

我們知道當你乘坐飛機飛行的時候你有很多選擇,這里我想表達的是在Flutter中選擇動畫,首先感謝你選擇使用AnimatedBuilderAnimatedWidget,等等,什么,還沒有使用?Flutter有很多不同的動畫widget,但是與商業航空公司不一樣的是,flutter中的每種類型的widget都有自己的適用場景。當然,你可以使用兩種不同的方式來完成一樣的動畫,但是使用適當的animation widget來完成這項工作,將會更加輕松。

這篇文章介紹了和其他動畫widget對比,你為什么可能需要使用AnimatedBuilderAnimatedWidget,以及如何使用它們,假設你想向你的APP中添加動畫。本文是該系列文章的一部分,逐步介紹了可能希望使用的各種類型的動畫widget。你想要特定動畫重復執行幾次,或者想要暫停、開始以響應某些事件,比如手指點擊,由于您的動畫需要重復或停止、開始,因此你將需要使用顯式動畫。

順便說一下,Flutter有兩大類型動畫:顯式和隱式。對于顯式動畫,你需要一個animation controller,對于隱式動畫則不需要。在上篇關于使用內置顯示動畫的文章,我們介紹了animation controller,假如你想要了解更多關于此的內容,請先查看那篇文章。

到此,如果你確定使用顯式動畫,有很多顯式動畫供您選擇,這些類通常命名為FooTransitionFoo是您想要設置的動畫的屬性名稱,我建議先了解一下是否可以使用其中的一個widget來實現你的需求,然后再深入了解AnimatedBuilderAnimatedWidget。有很多效果很棒的widget供您選擇,包括旋轉、位移、對齊、淡入淡出、文本樣式等,另外你可以組合這些Widget,這樣就可以同時進行旋轉和淡入淡出效果。但是,如果這些內置的Widget不能滿足你的需求,那么就是時機使用AnimatedBuilderAnimatedWidget了。

whichanimation.png

這是用于了解使用哪種動畫的流程圖,本文重點介紹底部的兩個藍色部分,AnimatedBuilder and AnimatedWidget。

特別的例子

為了使以上內容更加具體,讓我們來看一個具體的場景:我想編寫一個帶有外星飛船的APP,這個飛船有一個光柱動畫。


spaceship.gif

我繪制了一個漸變色的飛船光束,漸變色從正中心向外逐步黃色變為透明,然后,我使用路徑裁剪(path clipper)從該漸變創建了一個光束的形狀。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: MyHomePage(),
    ));
  }
}

class MyHomePage extends StatelessWidget {
  final Image starsBackground = Image.asset(
    'assets/milky-way.jpg',
  );
  final Image ufo = Image.asset('assets/ufo.png');
  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        starsBackground,
        ClipPath(
          clipper: const BeamClipper(),
          child: Container(
            height: 1000,
            decoration: BoxDecoration(
              gradient: RadialGradient(
                radius: 1.5,
                colors: [
                  Colors.yellow,
                  Colors.transparent,
                ],
              ),
            ),
          ),
        ),
        ufo,
      ],
    );
  }
}

class BeamClipper extends CustomClipper<Path> {
  const BeamClipper();

  @override
  getClip(Size size) {
    return Path()
      ..lineTo(size.width / 2, size.height / 2)
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height)
      ..lineTo(size.width / 2, size.height / 2)
      ..close();
  }

  /// Return false always because we always clip the same area.
  @override
  bool shouldReclip(CustomClipper oldClipper) => false;
}

我想要創建一個光束降落的動畫,從該漸變的中心開始,并使其重復。這意味著我需要創建顯式動畫,不幸的是,沒有內置的顯式動畫來為漏斗形漸變設置動畫,但是你知道我們有...AnimatedBuilderAnimatedWidget可以解決這個問題!

AnimatedBuilder

為了制作光束動畫,我將把這段漸變代碼包裹在AnimatedBuilder widget中。當AnimatedBuilder被調用的時候,包含在builder函數中漸變代碼也將被調用。

接下來我需要添加一個controller來驅動動畫,controller將會提供AnimatedBuilder用來逐幀繪制所需要的值。如你在之前的文章里看到的,我混入(mix in)了SingleTickerProviderStateMixin類,并在initState而不是build方法中初始化了controller實例對象,因為我不想多次創建controller--我想要它為動畫的每一幀提供新的值!因為我在initState中創建了一個新的對象,所以我也添加了一個dispose方法,用來告知Flutter,當不再有父節點widget顯示在屏幕上的時候,可以銷毀controller。

然后,我將controller傳遞給AnimatedBuilder,動畫按照預期運行啦!

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  final Image starsBackground = Image.asset(
    'assets/milky-way.jpg',
  );
  final Image ufo = Image.asset('assets/ufo.png');
  AnimationController _animation;

  @override
  void initState() {
    super.initState();
    _animation = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )..repeat();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        starsBackground,
        AnimatedBuilder(
          animation: _animation,
          builder: (_, __) {
            return ClipPath(
              clipper: const BeamClipper(),
              child: Container(
                height: 1000,
                decoration: BoxDecoration(
                  gradient: RadialGradient(
                    radius: 1.5,
                    colors: [
                      Colors.yellow,
                      Colors.transparent,
                    ],
                    stops: [0, _animation.value],
                  ),
                ),
              ),
            );
          },
        ),
        ufo,
      ],
    );
  }

  @override
  void dispose() {
    _animation.dispose();
    super.dispose();
  }
}

class BeamClipper extends CustomClipper<Path> {
  const BeamClipper();

  @override
  getClip(Size size) {
    return Path()
      ..lineTo(size.width / 2, size.height / 2)
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height)
      ..lineTo(size.width / 2, size.height / 2)
      ..close();
  }

  /// Return false always because we always clip the same area.
  @override
  bool shouldReclip(CustomClipper oldClipper) => false;
}

你可能還記得在TweenAnimationBuilder一文中,我們提到使用child 參數來進行性能優化,我們在AnimatedBuilder中也可以這樣做。基本上,如果我們在動畫中有從來沒改變過的對象,則可以提前構建他們,然后將它傳遞到AnimatedBuilder中。

在這個例子中,有一種更好的實現方式來做同樣的事情:給BeamClipper設置一個const構造函數,并且僅僅設置了const。這樣只需要少量的代碼,這個對象將會在編譯期創建,使構建更快速。當然,有時你會編寫一些沒有const構造函數的代碼,這種情況對與使用可選child參數來說是個很好的應用場景。

AnimatedWidget

到此,我們創建了自己的動畫,但是包含AnimatedBuilder的構建函數代碼量有點大,假如你的構建方法開始變的有點難以閱讀,是時候重構代碼了。

你可以將AnimatedBuilder代碼提取到單獨的Widget中,但是這樣的話,你的構建方法中將會嵌套另一個構建方法,看起來有點丑陋。取而代之的是,你可以通過繼承自AnimatedWidget創建一個新的Widget來完成相同的動畫。我將我的Widget命名為BeamTransition,與FooTransition顯示動畫的命名習慣一致。我將animation controller傳遞給BeamTransition,并重用了AnimatedBuilder構造函數的主體代碼。

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  final Image starsBackground = Image.asset(
    'assets/milky-way.jpg',
  );
  final Image ufo = Image.asset('assets/ufo.png');
  AnimationController _animation;

  @override
  void initState() {
    super.initState();
    _animation = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )..repeat();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        starsBackground,
        BeamTransition(animation: _animation),
        ufo,
      ],
    );
  }

  @override
  void dispose() {
    _animation.dispose();
    super.dispose();
  }
}

class BeamTransition extends AnimatedWidget {
  BeamTransition({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);
  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return ClipPath(
      clipper: const BeamClipper(),
      child: Container(
        height: 1000,
        decoration: BoxDecoration(
          gradient: RadialGradient(
            radius: 1.5,
            colors: [
              Colors.yellow,
              Colors.transparent,
            ],
            stops: [0, animation.value],
          ),
        ),
      ),
    );
  }
}

就像AnimatedBuilder一樣,如果可能的話,我將添加child參數到我的widget中,以便進行性能優化,因為它可以提前而不是每次進行動畫時進行構建。順帶提醒一下,在此例子中,將BeamClipper采用const構造聲明是最好的方式。

那么,我到底該用哪個吶?

我們剛剛看到了,當你無法找到內置顯式動畫想要實現你想要的效果時,AnimatedBuilderAnimatedWidget都可以用來實現相同效果的顯式動畫,那么,你該用哪一個吶?這是一個個人偏好問題,一般來說我建議制作獨立的widget,每個widget負責單獨的功能--在這個例子中是動畫。

絕大多數時,我都贊成使用AnimatedWidget,但是如果你創建animation controller的父節點Widget非常簡單,那么為你的動畫創建一個獨立的Widget可能會引入太多額外的代碼,這種情況,AnimatedBuilder是你的首選。

這里有這篇文章的視頻版本,如果你更喜歡視頻,點擊觀看

系列文章:

視頻 對應文章(英文原文) 對應文章(中文翻譯)
如何在 Flutter 中選擇合適的動畫 Widget?????在 Flutter中使用動畫的正確選擇 How to Choose Which Flutter Animation Widget is Right for You? 【已翻譯】鏈接
隱式動畫基礎 Flutter animation basics with implicit animations 【已翻譯】鏈接
使用 TweenAnimationBuilder 創建獨特的隱式動畫 Custom Implicit Animations in Flutter…with TweenAnimationBuilder 【已翻譯】鏈接
使用內置顯式動畫 Directional animations with built-in explicit animations 【已翻譯】鏈接
通過 AnimatedBuilder 和 AnimatedWidget 創建一個自定義動畫 When should I useAnimatedBuilder or AnimatedWidget? 【已翻譯】鏈接
深入理解動畫 Animation deep dive 【已翻譯】鏈接
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容