Flutter 動畫

暮鼓晨鐘發表於2021-08-31

AnimatedWidget簡化了什麼?

首先看一段不使用AnimatedWidget的程式碼: 給animation新增了Listener,動畫執行的每一幀都會回撥這個Listener,在這個Listener回撥中,呼叫setState()來完成widget的更新(重新整理)。

class ScaleAnimationRoute extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}

//需要繼承TickerProvider,如果有多個AnimationController,則應該使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>  with SingleTickerProviderStateMixin{ 
    
  Animation<double> animation;
  AnimationController controller;
    
  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
        setState(()=>{});
      });
    //啟動動畫(正向執行)
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return new Center(
       child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }

  dispose() {
    //路由銷燬時需要釋放動畫資源
    controller.dispose();
    super.dispose();
  }
}
複製程式碼

這樣是不是很麻煩,還需要手動呼叫setState()
那麼AnimatedWidget就是省略了setState()的步驟,AnimatedWidget類封裝了呼叫setState()的細節,來看下面的程式碼:

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

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }
}


class ScaleAnimationRoute1 extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
    with SingleTickerProviderStateMixin {

  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    //啟動動畫
    controller.forward();
  }

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

  dispose() {
    //路由銷燬時需要釋放動畫資源
    controller.dispose();
    super.dispose();
  }
}
複製程式碼

可以看到不再需要新增Listener並手動呼叫setState()方法了。AnimatedWidget 自己會使用當前 Animation 的 value 來繪製自己。

AnimatedBuilder是幹嘛的?

我們可以看到上面的AnimatedImage, 用AnimatedWidget可以從動畫中分離出widget,而動畫的渲染過程(即設定寬高)仍然在AnimatedWidget中。也就是animation.value仍然設定在Image的width,height屬性中。

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

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }
}
複製程式碼

AnimatedBuilder是在AnimatedWidget的基礎上將顯示內容和動畫拆分開來,更加方便的為特定的顯示內容新增具體的動畫。看下嘛的例子:ImageAnimation就完美的分開了。

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
      ),
    );
  }
}

...
Widget build(BuildContext context) {
    return GrowTransition(
    child: Image.asset("images/avatar.png"), 
    animation: animation,
    );
}
複製程式碼

AnimatedWidget和AnimatedBuilder就完美了嗎?

可以看到上面的程式碼,AnimationController依然暴露在外面,需要呼叫controller.forward()執行動畫,能不能把AnimationController封裝到動畫widget的內部,答案是肯定的,直接看程式碼:

class AnimatedDecoratedExampleBox extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AnimatedDecoratedExampleBoxState();
  }
}

class _AnimatedDecoratedExampleBoxState
    extends State<AnimatedDecoratedExampleBox> {
  Color _bgColor = Colors.blue;
  var duration = Duration(seconds: 1);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
            backgroundColor: Colors.grey[200],
            appBar: AppBar(
              title: Text("AnimatedSwitcher"),
            ),
            body: Center(
                child: AnimatedDecoratedBox(
              duration: duration,
              decoration: BoxDecoration(color: _bgColor),
              child: FlatButton(
                onPressed: () {
                  setState(() {
                      _bgColor = _bgColor == Colors.blue
              ? Colors.red
              : Colors.blue;
                  });
                },
                child: Text(
                  "AnimatedDecoratedBox",
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ))));
  }
}

class AnimatedDecoratedBox extends StatefulWidget {
  AnimatedDecoratedBox({
    Key key,
    @required this.decoration,
    this.child,
    this.curve = Curves.linear,
    @required this.duration,
    this.reverseDuration,
  });

  final BoxDecoration decoration;
  final Widget child;
  final Duration duration;
  final Curve curve;
  final Duration reverseDuration;

  @override
  _AnimatedDecoratedBoxState createState() => _AnimatedDecoratedBoxState();
}

class _AnimatedDecoratedBoxState extends State<AnimatedDecoratedBox>
    with SingleTickerProviderStateMixin {
  @protected
  AnimationController get controller => _controller;
  AnimationController _controller;

  Animation<double> get animation => _animation;
  Animation<double> _animation;

  DecorationTween _tween;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return DecoratedBox(
          decoration: _tween.animate(_animation).value,
          child: child,
        );
      },
      child: widget.child,
    );
  }

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      reverseDuration: widget.reverseDuration,
      vsync: this,
    );
    _tween = DecorationTween(begin: widget.decoration);
    _updateCurve();
  }

  void _updateCurve() {
    if (widget.curve != null)
      _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
    else
      _animation = _controller;
  }

  @override
  void didUpdateWidget(AnimatedDecoratedBox oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve) _updateCurve();
    _controller.duration = widget.duration;
    _controller.reverseDuration = widget.reverseDuration;
    if (widget.decoration != (_tween.end ?? _tween.begin)) {
      _tween
        ..begin = _tween.evaluate(_animation)
        ..end = widget.decoration;
      _controller
        ..value = 0.0
        ..forward();
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
複製程式碼

封裝了AnimatedDecoratedBox這樣一個動畫元件,在內部管理AnimationController,那它的動畫是何時執行的呢?

AnimatedDecoratedBox(
  duration: duration,
  decoration: BoxDecoration(color: _decorationColor),
  child: FlatButton(
    onPressed: () {
      setState(() {
        _decorationColor = Colors.red;
      });
    },
    child: Text(
      "AnimatedDecoratedBox",
      style: TextStyle(color: Colors.white),
    ),
  ),
)
複製程式碼

外部,點選按鈕時呼叫了setState()方法,那麼AnimatedDecoratedBox內部的didUpdateWidget()方法就會被呼叫,在這個方法裡面判斷新舊屬性不一樣,就執行動畫。

  @override
  void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve)
      _updateCurve();
    _controller.duration = widget.duration;
    _controller.reverseDuration = widget.reverseDuration;
    if(widget.decoration!= (_tween.end ?? _tween.begin)){
      _tween
        ..begin = _tween.evaluate(_animation)
        ..end = widget.decoration;
      _controller
        ..value = 0.0
        ..forward();
    }
  }
複製程式碼

關於didUpdateWidget()何時執行:

  • didUpdateWidget():在widget重新構建時,Flutter framework會呼叫Widget.canUpdate來檢測Widget樹中同一位置的新舊節點,然後決定是否需要更新,如果Widget.canUpdate返回true則會呼叫此回撥。正如之前所述,Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()就會被呼叫。如果key或runtime不一樣,整個widget state都會重構,initState()方法會重新執行。詳見我之前的文章:juejin.cn/post/684490…

上面是在didUpdateWidget()方法中進行手動控制的,flutter提供了兩個類簡化了這種控制: ImplicitlyAnimatedWidgetImplicitlyAnimatedWidgetState,詳見:book.flutterchina.club/chapter9/an…

AnimatedWidget,AnimatedBuilder同時執行多個動畫

同時執行大小和顏色透明度的動畫

class AnimatedImage extends AnimatedWidget {
  AnimatedImage({Key key, Animation<double> animation,this.animation_1, this.animation_2})
      : super(key: key, listenable: animation);

      final Animation<double> animation_1;
      final Animation<double> animation_2;

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: Container(
          width: animation_1.value,
          height: animation_1.value,
          color: Colors.orange.withOpacity(animation_2.value),
      ),
    );
  }
}


class ScaleAnimationRoute1 extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
    with SingleTickerProviderStateMixin {

  Animation<double> animation;
  AnimationController controller;
  Animation<double> animation_1;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);

    animation_1 = new Tween(begin: 0.0, end: 1.0).animate(controller);

    //啟動動畫
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedImage(animation: controller, animation_1:animation, animation_2: animation_1);
  }

  dispose() {
    //路由銷燬時需要釋放動畫資源
    controller.dispose();
    super.dispose();
  }
}
複製程式碼

交織動畫

有些時候我們可能會需要一些複雜的動畫,這些動畫可能由一個動畫序列或重疊的動畫組成,比如:有一個柱狀圖,需要在高度增長的同時改變顏色,等到增長到最大高度後,我們需要在X軸上平移一段距離。可以發現上述場景在不同階段包含了多種動畫,要實現這種效果,使用交織動畫(Stagger Animation)
待續

通用“動畫切換”元件(AnimatedSwitcher)

待續

自定義路由切換動畫

待續

Hero動畫

待續

參考文件:
book.flutterchina.club/chapter9/

相關文章