9.2 动画结构和状态监听

9.2.1 动画基本结构

Flutter中实现动画有很多在种,我们下边通过一个文本的旋转不同的实现方式演示动画。

基础版本

先看下效果:

我们实现一个红色方块从小变大的效果,首先看下基本的写法

class _ScaleANimationRouteState extends State<ScaleANimationRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  @override
  void initState() {
    _animationController = AnimationController(
        duration: Duration(milliseconds: 1000),
        lowerBound: 0,
        upperBound: 1.0,
        vsync: this)
      ..addListener(() {
        ///刷新UI
        setState(() {});
      })

      ///正向启动
      ..forward();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return _body();
  }

  Widget _body() {
    return Container(
      width: 100 * _animationController.value,
      height: 100 * _animationController.value,
      color: Colors.red,
    );
  }
  ///销毁
  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
}

controllerlisten中使用了setState(){}来刷新UI,含义是每次生成一个新的数字的时候,则调用一下setState(),setState会标记当前帧为dirty,会导致build方法再次调用,而在build中,方块的长和宽都乘了系数controlelr.value,所以就会被放大,当释放的时候需要手动调用_animationController.dispose();防止内存泄露。

上面是基础的效果,生成的值是线性的,那么我们使用Cruve指定一个弹簧效果,还需要改下initState代码

class ScaleANimationRoute extends StatefulWidget {
  ScaleANimationRoute({Key key}) : super(key: key);
  @override
  _ScaleANimationRouteState createState() => _ScaleANimationRouteState();
}

class _ScaleANimationRouteState extends State<ScaleANimationRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  Animation _animation;
  @override
  void initState() {
    _animationController = AnimationController(
        duration: Duration(milliseconds: 1000),
        lowerBound: 0.0,
        upperBound: 1.0,
        vsync: this)
      ..addListener(() {
        ///刷新UI
        setState(() {});
      });

    _animation =
        CurvedAnimation(parent: _animationController, curve: Curves.bounceOut);
    _animation = new Tween(begin: 0.0, end: 1.0).animate(_animation)
      ..addListener(() {
        setState(() {});
      });

    ///正向启动
    _animationController.forward();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return _body();
  }

  Widget _body() {
    return Container(
      width: 100.0 * _animation.value,
      height: 100.0 * _animation.value,
      color: Colors.red,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();

    super.dispose();
  }
}

使用AnimatedWidget封装

AnimatedWidget是官方提供的一个抽象类,AnimatedWidget内部已经实现了在Animated的监听_handleChange,我们看下_handleChange实现:

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

_handleChange其实内部是刷新UI的setState()函数。

这样子只需要把Animaiton的值传进来就可以实现动画了。代码重构后是这样子的:

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

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Container(
      width: 100 * animation.value,
      height: 100 * animation.value,
      color: Colors.lightBlueAccent,
    );
  }
}

调用的时候这样子调用


class _ScaleANimationRouteState extends State<ScaleANimationRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  Animation _animation;
  @override
  void initState() {
    _animationController = AnimationController(
        duration: Duration(milliseconds: 1000),
        lowerBound: 0.0,
        upperBound: 1.0,
        vsync: this);

    _animation =
        CurvedAnimation(parent: _animationController, curve: Curves.bounceOut);
    _animation = new Tween(begin: 0.0, end: 1.0).animate(_animation);

    ///正向启动
    _animationController.forward();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return _body();
  }

  Widget _body() {
    return AnimateWidgetFrame(
      animation: _animation,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();

    super.dispose();
  }
}

AnimateBuilder重构

AnimatedWidget可以从动画抽离widget,而动画的渲染过程仍然在AnimatedWidget中,假设我们再添加一个widget透明度的动画,那么我们需要再实现一个AnimatedWidget,这不是最好的选择,如果我们使用AnimateWidget,那么真正的渲染逻辑可以分开,上边的build函数改为:

  @override
  Widget build(BuildContext context) {
    print('刷新一次');
    int count = 0;
    return AnimatedBuilder(
      child: AnimateWidgetFrame(
        animation: _animation,
      ),
      animation: _animation,
      builder: (ctx, child) {
        print('动画刷新次数:${count++}');
        return Container(
          width: 100 * _animation.value,
          height: 100 * _animation.value,
          color: Colors.lightBlueAccent,
        );
      },
    );
  }

打印的数据;

flutter: 刷新一次
flutter: 动画刷新次数:0
......
flutter: 动画刷新次数:60

帧数基本在60浮动,而父级的widget只是刷新了一次,子部件是动画,刷新频率在61次?/秒,这样父部件刷新一次,节省了不少性能的开支。

上面child指定了一次,在build中又指定了一次,看起来是整理的2次,其实这是渲染和动画分开的结果,不管你是哪个child,在第一次指定之后,都可以实现build找那个的动画效果。

好处:

  • 没显示在父级setState(),渲染树可以只渲染子级的动画
  • 做到了child和动画的分离

那么再我们APP中这个动画很常用,那么需要再封装一层了。

class AnimatedLessWidget extends StatelessWidget {
  final Widget child;
  final Animation<double> animation;
  AnimatedLessWidget({this.child, this.animation});
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      child: child,
      animation: animation,
      builder: (ctx, w) {
        return Container(
          width: 100 * animation.value,
          height: 100 * animation.value,
          color: Colors.lightBlueAccent,
          child: w,
        );
      },
    );
  }
}

上面的build函数稍微改一下

@override
Widget build(BuildContext context) {
return AnimatedLessWidget(
  child: Text(
    '弹簧动画',
  ),
  animation: _animation,
);
}
}

9.2.2 动画监听

上面已经讲过了,AnimaitonaddStateListener是来监听动画状态变更的,Flutter中有四种动画状态,他们分别是

枚举值 含义
dismissed 动画在起点停止
forward 动画正向开始
reverse 动画反向执行
completed 动画在终点停止

例子

我们实现一个正向和反向不断循环的例子

完整代码:


class _ScaleANimationRouteState extends State<ScaleANimationRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  Animation _animation;
  @override
  void initState() {
    _animationController = AnimationController(
        duration: Duration(milliseconds: 1000),
        lowerBound: 0.0,
        upperBound: 1.0,
        vsync: this)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          /// 如果结束,则反向运动
          _animationController.reverse();
        } else if (status == AnimationStatus.dismissed) {
          // 如果反向结束 则正向开始
          _animationController.forward();
        }
      });

    _animation =
        CurvedAnimation(parent: _animationController, curve: Curves.bounceOut);
    _animation = new Tween(begin: 0.0, end: 1.0).animate(_animation);

    ///正向启动
    _animationController.forward();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedLessWidget(
      child: Text(
        '弹簧动画',
      ),
      animation: _animation,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();

    super.dispose();
  }
}

/// 封装 部件和效果
class AnimatedLessWidget extends StatelessWidget {
  final Widget child;
  final Animation<double> animation;
  AnimatedLessWidget({this.child, this.animation});
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      child: child,
      animation: animation,
      builder: (ctx, w) {
        return Container(
          width: 100 * animation.value,
          height: 100 * animation.value,
          color: Colors.lightBlueAccent,
          child: w,
        );
      },
    );
  }
}

最终效果是:

9.2.3 FadeTransition、SlideTransition、RotationTransition、SizeTransition

这几个是官方预置的几个效果,我们分别写一下例子:

FadeTransition

FadeTransition是一个渐变的效果,透明度从0到1,当然我们也可以自行处理让他范围变小。


  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _animation,
      child: Container(
        color: Colors.lightBlueAccent,
        width: 100,
        height: 100,
      ),
    );
  }

效果:

SlideTransition

SlideTransition是一个位移的动画,位移参数position的系数的自身的大小X系数大小,。


_animation = new Tween<Offset>(begin: Offset(0, 0), end: Offset(1.0, 1.0))
        .animate(_animation)

@override
Widget build(BuildContext context) {
return SlideTransition(
  position: _animation,
  child: Container(
    color: Colors.lightBlueAccent,
    width: 100,
    height: 100,
  ),
);
}

效果:

results matching ""

    No results matching ""