Flutter 动画

838 阅读4分钟

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/