Flutter和Dart系列十二:動畫(Animation)

一個App中如果能有優秀的動畫效果,能讓App看起來顯得更加高大上。此篇我們就來介紹一下Flutter中Animation體系。

  1. 我們先來一個簡單的例子,來實現透明度漸變動畫:

    class FadeInDemo extends StatefulWidget {
      @override
      State createState() {
        return _FadeInDemoState();
      }
    }
    
    class _FadeInDemoState extends State {
      double opacity = 0.0;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            MaterialButton(
              child: Text(
                "Click Me",
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                opacity = 1;
              }),
            ),
            AnimatedOpacity(
                duration: const Duration(seconds: 2),
                opacity: opacity,
                child: Text('Flutter Animation Demo01')
                )
          ],
        );
      }
    }

    這裏我們藉助於AnimatedOpacity來實現漸變:我們通過點擊按鈕來觸發動畫效果,如下圖所示。

    我們來總結一下實現步驟:

    • 使用AnimatedOpacity來包裹需要實現透明度漸變動畫的Widget,並指定duration和opacity參數。這倆參數也好理解:duration自然是動畫時間,opacity表示透明度(取值範圍爲0~1,0表示透明)
    • 觸發動畫:通過setState()方法,我們可以直接指定opacity的最終值(爲1,即完全顯示)。因爲所謂的動畫,肯定是有起始狀態和結束狀態,然後在指定的動畫時間內慢慢發生變化。

    2.使用AnimatedContainer來實現其他屬性變化的動畫.

        class AnimatedContainerDemo extends StatefulWidget {
          @override
          State createState() {
            return _AnimatedContainerDemoState();
          }
        }
    
        class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
          Color color;
          double borderRadius;
          double margin;
    
          double randomBorderRadius() {
            return Random().nextDouble() * 64;
          }
    
          double randomMargin() {
            return Random().nextDouble() * 64;
          }
    
          Color randomColor() {
            return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
          }
    
          void change() {
            setState(() {
              initState();
            });
          }
    
          @override
          void initState() {
            color = randomColor();
            borderRadius = randomBorderRadius();
            margin = randomMargin();
          }
    
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: Center(
                child: Column(
                  children: <Widget>[
                    SizedBox(
                      width: 128,
                      height: 128,
                      child: AnimatedContainer(
                        curve: Curves.easeInOutBack,
                        duration: const Duration(milliseconds: 400),
                        margin: EdgeInsets.all(margin),
                        decoration: BoxDecoration(
                          color: color,
                          borderRadius: BorderRadius.circular(borderRadius),
                        ),
                      ),
                    ),
                    MaterialButton(
                      color: Theme.of(context).primaryColor,
                      child: Text(
                        'change',
                        style: TextStyle(color: Colors.white),
                      ),
                      onPressed: () => change(),
                    )
                  ],
                ),
              ),
            );
          }
        }

    運行效果如下:

    我們這次同時修改了margin、color以及borderRadius三個屬性。AnimatedContainer的使用思路和AnimatedOpacity類似:

    • 包裹子widget,指定duration及相關屬性參數
    • 在setState方法中指定屬性的動畫終止狀態

實際上我們剛剛介紹的兩種實現方式被稱之爲隱式動畫(implicit animation),可以理解成對於Animation子系統進行了一層封裝,方便我們開發者使用。下面我們正式來介紹Animation子系統的重要組成類:

3.Animation類:通過這個類,我們可以知道當前的動畫值(animation.value)以及當前的狀態(通過設置對應的監聽listener),但是對於屏幕上如何顯示、widget如何渲染,它是不知道的,換句話說也不是它所關心的,這點從軟件設計上耦合性也更低。其次,從代碼角度上,它是一個抽象類:

4.AnimationController類。

從類本身上看,它是繼承自Animation類的,並且泛型類型爲double。從作用上來看,我們可以通過AnimationController來指定動畫時長,以及它提供的forward()、reverse()方法來觸發動畫的執行。

5.Curved­Animation類:同樣繼承自Animation類,並且泛型類型爲double。它主要用來描述非線性變化的動畫,有點類似Android中的屬性動畫的插值器。

6.Tween類.

從類的層次結構上,它有所不同,不再是繼承自Animation,而是繼承自Animatable。它主要用來指定起始狀態和終止狀態的。

好,我們已經對這四個類有了一定的瞭解,下面我們就從實例來看看他們是如何結合在一起使用的。

7.實例一:

class LogoDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {

 // 註釋1:這裏已經出現了我們前面提到的Animation和AnimationController類
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    // 註釋2:
    // 在構造一個AnimationController對象時,我們需要傳遞兩個參數
    // vsync:主要爲了防止一些超出屏幕之外的動畫而導致的不必要的資源消耗。我們這裏就傳遞
    //        this,除此之外,我們還需要使用with關鍵字來接入SingleTickerProviderStateMixin類型
    // duration:指定動畫時長
    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));

    // 註釋3: 通過Tween對象來指定起始值和終止值,並且通過animate方法返回一個Animation對象,
    //             並且設置了監聽,最後在監聽回調中調用setState(),從而刷新頁面
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {});
      });

     // 註釋4: 啓動動畫 
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        // 註釋5:通過Animation對象類獲取對應的變化值
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
  // 註釋6:對controller解註冊
    controller.dispose();
    super.dispose();
  }
}

運行效果如圖所示:

這裏涉及到了兩個Dart語言本身的知識點:mixin和..。mixin這裏推薦一篇medium上的文章:https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3;而..很簡單,它就是爲了方便鏈式調用。我們可以看下前面addListener()方法,方法本身返回的void類型,但是我們最終卻返回了一個Animation類型的對象。這其中就是..起到的作用,它可以使得方法返回調用這個方法的對象本身。

8.使用AnimatedWidget來重構上面的代碼:

class LogoDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }

        print('$status');
      });

    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedLogo(animation: animation);
  }

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


class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

先看效果:

大部分代碼是和之前的例子是一樣的,不同的是:

  • 使用AnimatedWidget,並且Animation對象作爲參數傳遞進來
  • 省略了在addListener的回調裏調用setState方法來觸發頁面刷新

這樣寫的好處:

  • 省去了需要調用setState的重複代碼
  • 使得程序耦合性更低。試想一下,我們的App中有多處都需要實現Logo的resize動畫,這個時候我們只需要在使用處定義Animation的描述,最後都使用這裏的AnimatedLogo。這樣做就使得Widget和Animation的描述進行分離。

我們可以去看一下AnimatedWidget類的源碼:

abstract class AnimatedWidget extends StatefulWidget {
  /// Creates a widget that rebuilds when the given listenable changes.
  ///
  /// The [listenable] argument is required.
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);

  /// The [Listenable] to which this widget is listening.
  ///
  /// Commonly an [Animation] or a [ChangeNotifier].
  final Listenable listenable;

  /// Override this method to build widgets that depend on the state of the
  /// listenable (e.g., the current value of the animation).
  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
  }
}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

可以看到在initState方法裏,添加了動畫監聽,回調的執行邏輯爲_handleChange()方法,而_handleChange()的實現就是調用了setState方法,這點和我們之前在第7條中例子的寫法一樣。也就是說,AnimatedWidget只是做了一層封裝而已。

9.使用AnimatedBuilder進一步重構上面的代碼:

class LogoDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return GrowTransition(child: LogoWidget(), animation: animation);
  }

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


class LogoWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 10),
      child: FlutterLogo(),
    );
  }
}

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
          animation: animation,
          builder: (context, child) => Container(
            height: animation.value,
            width: animation.value,
            child: child,
          ),
          child: child),
    );
  }
}

我們可以在任意地方使用這裏GrowTransition,代碼進行進一步分離。

10.使用Transition。

Flutter還爲我們提供一些封裝好的Transition,方便我們實現動畫效果,下面我們就以ScaleTransition爲例,說明如何去使用這些Transition。

class ScaleTransitionDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();

    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    animation = Tween<double>(begin: 0, end: 10).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
        child: ScaleTransition(
      scale: animation,
      child: FlutterLogo(),
    ));
  }

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

ScaleTransition在使用時需要指定兩個參數:

  • scale: 就是一個Animation對象
  • child: 需要實現縮放動畫的widget

最後再注意一下Tween中指定的值所表示的含義,它表示的倍數。比如我們這裏end填入了10,表示動畫結束狀態爲放大10倍。我們可以通過ScaleTransition的源碼來說服大家:

class ScaleTransition extends AnimatedWidget {
  /// Creates a scale transition.
  ///
  /// The [scale] argument must not be null. The [alignment] argument defaults
  /// to [Alignment.center].
  const ScaleTransition({
    Key key,
    @required Animation<double> scale,
    this.alignment = Alignment.center,
    this.child,
  }) : assert(scale != null),
       super(key: key, listenable: scale);

  /// The animation that controls the scale of the child.
  ///
  /// If the current value of the scale animation is v, the child will be
  /// painted v times its normal size.
  Animation<double> get scale => listenable;

  /// The alignment of the origin of the coordinate system in which the scale
  /// takes place, relative to the size of the box.
  ///
  /// For example, to set the origin of the scale to bottom middle, you can use
  /// an alignment of (0.0, 1.0).
  final Alignment alignment;

  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.child}
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final double scaleValue = scale.value;
    final Matrix4 transform = Matrix4.identity()
      ..scale(scaleValue, scaleValue, 1.0);
    return Transform(
      transform: transform,
      alignment: alignment,
      child: child,
    );
  }
}

可以看到,ScaleTransition也是繼承自我們前面已經介紹過的AnimatedWidget,然後重點關注build()方法裏,用到了Matrix4矩陣,這裏的scale.value實際上就是Animation.value,而Matrix4.identity()..scale(),它的三個參數分別表示在x軸、y軸以及z軸上縮放的倍數。

與ScaleTransition類似的還有SlideTransition、RotationTransition等等,讀者可以自己去嘗試一下,這裏就不在一一贅述了。

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