Flutter補間動畫

正義啊發表於2019-08-23

作為一個移動端UI框架,Flutter 也擁有自己的動畫體系。

分類

Flutter 動畫分為兩類:補間動畫(Tween)和 基於物理的動畫。

本文主要介紹第一類動畫。

動畫的基本類

Animation<T>

Animation是一個抽象的類,主要儲存動畫的狀態和當前值。最常用的Animation類是Animation<double>

T 有很多型別,如Color、Offset。後面會詳細介紹

可以通過Animation中的 value 屬性獲得當前動畫的值。

動畫的監聽:

  • addListener() 每一幀動畫執行的監聽
  • addStatusListener() 動畫狀態改變的監聽。有下面四種狀態
    在這裡插入圖片描述
AnimationController

AnimationController 繼承Animation<double>,負責控制動畫的執行,停止等。

AnimationController 會在動畫的每一幀,就會生成一個新的值。預設情況下,AnimationController在給定的時間段內線性的生成從0.0到1.0(預設區間)的數字。

建立 AnimationController,則需要傳入一個 vsync 引數。

Tween

預設情況下,AnimationController物件的範圍從0.0到1.0。

如果你需要不同範圍或者不同的資料型別,就需要tween來配置動畫以生成不同的範圍或資料型別的值

Tween的子類如下圖所示:

在這裡插入圖片描述

例子
class PageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController controller;
  //doubler型別動畫
  Animation<double> doubleAnimation;
  //顏色動畫
  Animation<Color> colorAnimation;
  //位置動畫
  Animation<Offset> offsetAnimation;
  //圓角動畫
  Animation<BorderRadius> radiusAnimation;
  //裝飾動畫
  Animation<Decoration> decorationAnimation;
  //字型動畫
  Animation<TextStyle> textStyleAnimation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //建立AnimationController
    controller = new AnimationController(
        vsync: this, duration: Duration(milliseconds: 2000));
    //animation第一種建立方式:
    doubleAnimation = new Tween<double>(begin: 0.0, end: 200.0).animate(controller)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((AnimationStatus status) {
      	//執行完成後反向執行
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
        //反向執行完成,正向執行
          controller.forward();
        }
      });
    //animation第二種建立方式:
    offsetAnimation = controller.drive(
      Tween<Offset>(begin: Offset(0.0, 0.0),end: Offset(400.0, 200.0))
    );
   
    colorAnimation =  ColorTween(begin: Colors.yellow,end: Colors.red).animate(controller);
    radiusAnimation = BorderRadiusTween(begin: BorderRadius.circular(0),end: BorderRadius.circular(50)).animate(controller);
    decorationAnimation = DecorationTween(begin: BoxDecoration(color: Colors.purple,borderRadius: BorderRadius.circular(0),),
        end: BoxDecoration(color: Colors.lightBlueAccent,borderRadius: BorderRadius.circular(40))).animate(controller);
    textStyleAnimation = TextStyleTween(begin: TextStyle(color: Colors.black,fontSize: 20,fontWeight: FontWeight.w100),
        end: TextStyle(color: Colors.purple,fontSize: 30,fontWeight: FontWeight.w700)).animate(controller);
    //啟動動畫
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: Text("Tween動畫"),),
      body: Container(
        alignment: Alignment.center,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 200,
              child:  Container(
                height: doubleAnimation.value,
                width: doubleAnimation.value,
                child: FlutterLogo(),
              ),
            ),
            Container(
              margin: EdgeInsets.only(left: offsetAnimation.value.dx),
              width: 50,
              height: 50,
              color: Colors.green,
            ),
            Container(
              height: 100,
              width: 100,
              color: colorAnimation.value,
            ),
            SizedBox(height: 10,),
            Container(
              height: 100,
              width: 100,
              decoration: BoxDecoration(borderRadius: radiusAnimation.value,color: Colors.blue),
            ),
            Container(
              height: 60,
              width: 200,
              decoration: decorationAnimation.value,
            ),
            Container(
              height: 100,
              child: Text("TestStyleTween",style: textStyleAnimation.value,),
            ),

          ],
        ),
      )
    );
  }
  
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    controller.dispose();
  }
}
複製程式碼
tween動畫執行效果圖

在這裡插入圖片描述
這裡列舉裡幾種比較簡單的Tween動畫。在上面的程式碼中我們通過對animation設定addListener()對每一幀的變化進行監聽,當animation 中的value插值發生改變時呼叫 setState(() {});重新整理佈局,從而達到動畫過度的效果。

當我們state中的佈局複雜的時候,我們在每一幀變化的時候都呼叫setState來重新整理widget樹,會把state中所有的widget都重新繪製,這樣就會造成不必要的效能消耗,我們只需要重新整理執行動畫的那個widget就行了。Flutter為我們提供了AnimatedWidget。

AnimatedWidget

原始碼
abstract class AnimatedWidget extends StatefulWidget {
	//建立一個widget,當listenable 發生改變時重構
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);
  //宣告一個Listenable ,幀動畫監聽
  final Listenable listenable;

  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();
...
}
複製程式碼

AnimatedWidget繼承自StatefulWidget,擁有自己的狀態,並且例項一個listenable用來監聽幀動畫,當動畫方法變化時,重新整理AnimatedWidget。

__AnimatedState方法原始碼如下:

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.
    });
  }
//呼叫build()方法,重構AnimatedWidget
  @override
  Widget build(BuildContext context) => widget.build(context);
}
複製程式碼

當listenable觸發重新整理的時候,呼叫 setState重構AnimatedWidget,雖然到最後還是呼叫setState,但是重新整理的物件是不同的。

Listenable

從原始碼中可以看到在AnimatedWidget宣告瞭一個Listenable,用來監聽每一幀的變化。那麼Listenable又是啥呢。我們可以看一小截Animation的原始碼:

在這裡插入圖片描述
可以看到Animation也是繼承自Listenable,Listenable 原始碼如下:

abstract class Listenable {

  const Listenable();

  factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;

  void addListener(VoidCallback listener);

  void removeListener(VoidCallback listener);
}
複製程式碼

所以Animation也是一個Listenable 實現類。原始碼看完,具體用法如下:

ColorAnimationWidget

宣告一個ColorAnimationWidget類,繼承自AnimatedWidget。程式碼如下:

class ColorAnimationWidget extends AnimatedWidget{

  ColorAnimationWidget({Key key, Animation<Color> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<Color> animation = listenable;
    // TODO: implement build
    return Center(
      child: Container(
        width: 200,
        height: 200,
        color: animation.value,
      ),
    );
  }
}
複製程式碼
使用ColorAnimationWidget
class PageState extends State<HomePage> with SingleTickerProviderStateMixin{

  AnimationController _controller;
  Animation<Color> _animation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 2));
    _animation = ColorTween(begin: Colors.lightBlueAccent,end: Colors.red).animate(_controller)
      ..addStatusListener((AnimationStatus status){
        if(status == AnimationStatus.completed){
          _controller.reverse();
        }else if(status == AnimationStatus.dismissed){
          _controller.forward();
        }
      });
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      body: ColorAnimationWidget(animation: _animation,),
    );
  }
}

class ColorAnimationWidget extends AnimatedWidget{

  ColorAnimationWidget({Key key, Animation<Color> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<Color> animation = listenable;
    // TODO: implement build
    return Center(
      child: Container(
        width: 200,
        height: 200,
        color: animation.value,
      ),
    );
  }
}
複製程式碼

可以看到用法基本和不使用AnimatedWidget一樣,唯一的區別就是使用AnimatedWidget時setState是在AnimatedWidget內呼叫的,只重新整理一個widget。

ColorAnimationWidget效果圖:

在這裡插入圖片描述

CurvedAnimation

Tween動畫預設為我們提供了區間內線性變化,如果我們需要曲線變化,則需要配合使用CurvedAnimation。如下圖彈性效果:

在這裡插入圖片描述
用法如下:

class HomePageState extends State<HomePage> with TickerProviderStateMixin{

  Animation animation;
  AnimationController controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
    CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = Tween<double>(begin: 0.0,end: 500).animate(curve)
      ..addListener((){
        setState(() {
        });
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      alignment: Alignment.topCenter,
      child: Container(
        margin: EdgeInsets.only(top: animation.value),
        width: 100,
        height: 100,
        child: FlutterLogo(),
      ),
    );
  }
}
複製程式碼

CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut); 這裡我們使用的是bounceOut效果

更多的曲線效果可以檢視官網的效果圖:Curves

我們看一下bounceOut是如何實現的:

class _BounceOutCurve extends Curve {
  const _BounceOutCurve._();

  @override
  double transformInternal(double t) {
    return _bounce(t);
  }
}

double _bounce(double t) {
  if (t < 1.0 / 2.75) {
    return 7.5625 * t * t;
  } else if (t < 2 / 2.75) {
    t -= 1.5 / 2.75;
    return 7.5625 * t * t + 0.75;
  } else if (t < 2.5 / 2.75) {
    t -= 2.25 / 2.75;
    return 7.5625 * t * t + 0.9375;
  }
  t -= 2.625 / 2.75;
  return 7.5625 * t * t + 0.984375;
}
複製程式碼

可以看到bounceOut 是繼承 Curve類,實現它的transformInternal方法,在transformInternal實現它的軌跡。

AnimatedBuilder

上面我們實現了一個顏色變化的例子,假如我們現在需要實現一個大小變化的widget呢?是不是要在宣告一個SizeAnimationWidget繼承AnimationWidget ?

顯然這樣做是可以的。當然我們也有更好的做法,就是使用AnimatedBuilder來重構我們的widget。

什麼是AnimatedBuilder

AnimatedBuilder繼承自抽象的AnimationWidget ,目的為了構建通用的AnimationWidget 實現類,不用每次使用AnimationWidget 都要建立一個實現類。

AnimatedBuilder原始碼:
class AnimatedBuilder extends AnimatedWidget {
  /// Creates an animated builder.
  const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}
複製程式碼

使用的時候我們只需要傳入animation和builder就行了

用法
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  Animation animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
    controller.forward();
  }

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

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

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

  final Widget child;
  final Animation<double> animation;

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

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 10.0),
      child: new FlutterLogo(),
    );
  }
}
複製程式碼
效果圖

在這裡插入圖片描述

總結

tween動畫到這裡就結束了,Hero動畫留下一次在補充吧

參考:Flutter動畫教程

相關文章