AnimationController的几种应用

126 阅读7分钟

AnimationController的大概作用是在一定的时间内,按照屏幕刷新率(帧频)在指定的范围内按照顺序(顺序、倒叙)产生一些数据。默认是产生0到1之间的数字。

基本概念

AnimationController实例化的时候至少需要两个东西,duration用于指定时长,vsync用于提供TickerProvider,TickerProvider就像一个时钟,可以以固定的频率产生事件。往往我们通过组合SingleTickerProviderStateMixin类让当前类变成一个TickerProvider。如下初始化过程:

class MyState extends State<MyStatefulWidget> 
                                    with SingleTickerProviderStateMixin {
    late AnimationController _controller;

    @override
    void initState() {
        super.initState();
        _controller = AnimationController(
            vsync: this,
            duration: const Duration(seconds: 10)
        );
        _controller.forward();
    }
}

我们在initState函数中实例化AnimationController,并通过forward方法启动AnimationController,它将在10秒内以固定的频率不断产生0到1之间的数字。这里需要留意的是要在initState函数中初始化。

接下来就可以通过添加listener来查看其产生的值,如下

@override
void initState() {
     super.initState();
     _controller = AnimationController(
         vsync: this,
         duration: const Duration(seconds: 10)
     );
     _controller.addListener((){
        debugPrint('${_controller.value}, --');
     });
     _controller.forward();
}

控制台看到类似如下的输出就对了

I/flutter (13413): 0.9610909, --
I/flutter (13413): 0.9649901, --
I/flutter (13413): 0.9705375999999999, --
I/flutter (13413): 0.9749848999999999, --
I/flutter (13413): 0.9783161, --
I/flutter (13413): 0.9816525, --
I/flutter (13413): 0.9833181, --
I/flutter (13413): 0.9883176, --
I/flutter (13413): 0.9916456, --
I/flutter (13413): 0.9949795, --
I/flutter (13413): 1.0, --

有了上面的这些随时间变化的数字,我们就可以将其应用到widget中,改变widget的颜色,形状,位置等等继而形成动画。

应用一: 改变文本的fontSize

先创建一个App类命名为TestApp1,继承StatelessWidget,重写build方法,返回MaterialAppMaterialApp的home指定为TestAppHome实例。

class TestApp1 extends StatelessWidget {
  const TestApp1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.orange
      ),
      title: 'TestApp1',
      home: const TestAppHome(),
    );
  }
}

TestAppHome类继承StatefulWidget,并重写createState方法,返回TestAppState实例。

class TestAppHome extends StatefulWidget {
  const TestAppHome({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return TestAppState();
  }
}

class TestAppState extends MyAnimationState {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    double v = controller.value;
    return Scaffold(
      appBar: AppBar(
        title: const Text('TestApp1'),
      ),
      body: Container(
        alignment: Alignment.center,
        child: Text('hello', style: TextStyle(fontSize: 60+v*30, color: DataUtil.randomColor),),
      ),
    );
  }
}

TestAppState类继承MyAnimationController类,注意15行的变量v,将其应用到22行的TextStylefontSize。下面看看MyAnimationController的实现

import 'package:flutter/material.dart';

class MyAnimationState extends State with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  get controller { return _controller; }

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this,
        duration: const Duration(seconds: 1)
    );
    _controller.addListener(onTick);
    _controller.repeat(reverse: true);
  }

  void onTick() {
    setState(() {

    });
  }

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

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }

}

这里代码具体的解释就不多说了,一看就懂,最终效果:

应用二:改变组件的scale属性

这里重用上面的TestApp1,TestAppHomeMyAnimationState类,创建TestApp2State继承MyAnimationState,然后重写build方法

class TestApp2State extends MyAnimationState {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TestApp2'),
      ),
      body: Center(
        child: AnimatedBuilder(
            animation: controller,
            builder: (context, child) {
                return Transform.scale(
                  scale: controller.value,
                  child: child,
                );
            },
            child: Container(
              width: 150,
              height: 150,
              color: DataUtil.randomColor
            ),
        ),
      ),
    );
  }
}

应用一中是在listener的处理函数中调用setState方法促使build方法执行,这次是利用AnimatedBuilder类来直接处理需要做动画的子组件,将controller.value直接赋值给Transform.scale方法的scale参数。最终的效果如下:

应用三:设置color属性改变颜色

在应用二中Container的颜色也是在不断变化的,那是因为每次build执行时,随机设置了Containercolor属性,所以看上去像是在闪烁的效果。而应用三要实现的颜色变化是一个颜色过渡动画,即从一种颜色过渡到另一种颜色。

要实现颜色过渡动画,我们可以借助ColorTween类。在initState中初始化ColorTween

// 声明变量
late Animation<Color?> _colorTween;

@override
void initState() {
  // TODO: implement initState
  super.initState();
  _colorTween = ColorTween(
      begin: Colors.white,
      end: Colors.black
  ).animate(controller);
}

ColorTween初始化时只需要传入一个开始颜色和一个结束颜色,然后调用它的animate方法,并将之前的AnimationController实例传入即可。至此颜色渐变动画创建完成,下面将其应用于组件的color属性

@override
Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: const Text('TestApp3'),),
      body: Center(
          child: AnimatedBuilder(
            animation: controller,
            builder: (context, child) {
              return Container(
                  width: 200,
                  height: 200,
                  color: _colorTween.value
              );
            }
          ),
      ),
    );
}

跟应用二不同的地方是,AnimatedBuilder我们没有设置child,而是直接在builder函数中返回了child。最终的效果如下:

不过如果我们想再给child增加旋转,缩放,移动等动画呢?这时就可以像应用二中将AnimatedBuilder的child赋值,在build函数中调用对于的函数,如下

@override
Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: const Text('TestApp3'),),
      body: Center(
          child: AnimatedBuilder(
            animation: controller,
            builder: (context, child) {
              return Transform.rotate(
                angle: controller.value,
                child: child,
              );
            },
            child: Container(
                width: 200,
                height: 200,
                color: _colorTween.value
            ),
          ),
      ),
    );
}

最终效果:

应用四:形变动画的实现

通过BoxDecoration我们可以设置容器的边框,圆角,形状等,那我们也可以将其改变应用到动画中。这里用到DecorationTween,实例化DecorationTween类需要一个开始和结束的BoxDecoration,看下面的代码

@override
void initState() {
    // TODO: implement initState
    super.initState();
    _decorationTween = DecorationTween(
        begin: BoxDecoration(
            color: Colors.red,
            border: Border.all(color: Colors.blue, width: 4),
            borderRadius: BorderRadius.circular(15)
        ),
        end: BoxDecoration(
            color: Colors.lightGreen,
            border: Border.all(color: Colors.amber, width: 4),
            borderRadius: BorderRadius.circular(50)
        ),
    ).animate(controller);
}

上面动画设置为边框颜色从blue到amber,圆角半径从15到50,下面需要将其应用到组件上

@override
Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
            title: const Text('TestApp4'),
        ),
        body: Center(
            child: DecoratedBoxTransition(
                decoration: _decorationTween,
                child: Container(
                    width: 200,
                    height: 200,
                ),
            ),
        )
    );
}

build中用DecoratedBoxTransition类来渲染需要产生动画的容器。DecoratedBoxTransition的child为产生动画的容器,decoration为前面的DecorationTween实例,具体效果如下:

应用五:与Flow组件的结合

这个应用的本质跟第一个其实一样,利用controller不同的value值,每次重写调用build函数。Flow组件要求两个必须参数,一个是delegate,一个是children。先创建MyFlowDelegate类

import 'dart:math';

import 'package:flutter/material.dart';

class MyFlowDelegate extends FlowDelegate {

  double value = 0;

  MyFlowDelegate({this.value = 0}):super();

  @override
  void paintChildren(FlowPaintingContext context) {
    // TODO: implement paintChildren
    double x = context.size.width/2;
    double y = context.size.height/2;

    for(var i=0;i<context.childCount;i++) {
      double r = 45;
      double cx = x - (context.getChildSize(i)?.width??0)/2;
      double cy = y - (context.getChildSize(i)?.height??0)/2;
      double radius = 150;
      double angle = (i/context.childCount)*2*pi+value;
      Matrix4 matrix4 = Matrix4.translationValues(
          cx + cos(angle)*radius,
          cy + sin(angle)*radius,
          0
      );
      // matrix4.rotateX(pi/4);
      context.paintChild(i, transform: matrix4);
    }
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }
}

需要重写paintChildren和shouldRepaint两个方法,第一个是在每次build的时候会执行。我们在paintChildren中精确地设置每一个child的位置。接下来在Home类中创建children

class TestApp5Home extends StatefulWidget {

  List<Widget> _list = [];

  TestApp5Home({Key? key}) : super(key: key) {
    _list = createFlowChildren(10);
  }

  get list => _list;

  List<Widget> createFlowChildren(int len) {
    return List.generate(len, (index) {
      return Container(
        width: 50,
        height: 50,
        color: DataUtil.randomColor2,
      );
    });
  }

  @override
  State<StatefulWidget> createState() {
    return TestApp5State();
  }
}

createFlowChildren函数创建了多个宽高为50的Container。下面将其应用到Flow组件

class TestApp5State extends MyAnimationState {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TestApp5'),
      ),
      body: Flow(
        delegate: MyFlowDelegate(value: controller.value),
        children: (widget as TestApp5Home).list,
      ),
    );
  }
}

MyAnimationState类的实现跟前面是一样的,这里我们只用到了controller的value值。最终效果:

总结

  • AnimationController可以有两种应用方式:
  1. 通过listener方法监听value的每次改变,然后在监听函数中调用setState促使build方法的每次调用,而每次在build中读取value值,且将value值设置到各个组件的属性中,如fontSize,scale,color等等。

  2. 不用每次调用setState促使多次build调用,而是利用AnimatedBuilder或者DecoratedBoxTransition与其结合自动处理子组件的动画效果。

  • 创建动画的必要步骤:
  1. 创建State子类,同时通过with关键字组合SingleTickerProviderStateMixin类。

  2. 在initState方法中实例化AnimationController,设置初始的lowerbound、upperBound、duration、vsync,如果要自己促使build多次调用读取value值,就可以调用addListener方法,并在回调中调用setState,否则无需。

  3. 调用AnimationController的forward,或者repeat方法启动动画,此时AnimationController已经根据初始的lowerbound、upperBound,duration产生了不同的数据,如果调用addListener就可以在回调函数中看到。

  4. 重写dispose方法,调用AnimationController的dispose方法销毁。

  • 应用1:

在listener的回调函数中直接调用setState,意思就是在AnimationController的value每次改变的时候重新调用build,然后在build中读取value值,将其赋值给Text的fontSize,所以我们能看到文本的大小改变的动画了。

  • 应用2:

在State的initState方法中实例化AnimationController时,没有监听value的改变,没有调用setState,而是在build方法中通过AnimatedBuilder来处理子组件的动画。将AnimationController实例赋值给AnimatedBuilder的animation属性,同时builder方法通过Transform的scale,translate,rotate来设置动画。

  • 应用3:

与应用2不同的是,这里并没有调用Transform的方法,只是在AnimatedBuilder的builder中将controller的value值赋值给了子组件的color。Transform的scale,translate,rotate分别用于缩放、平移、旋转子组件,这里只是改变子组件颜色,所以无需Transform类。

  • 应用4:

也是没有直接调用setState促使多次build调用,而是利用DecoratedBoxTransition结合各自Tween类来实现子组件的动画。类似的Transition类还有:

AlignTransition
DefaultTextStyleTransition
PositionedTransition
RelativePositionedTransition
RotationTransition
ScaleTransition
SizeTransition
SlideTransition
FadeTransition

关于Tween相关的类还有:

AlignmentGeometryTween
AlignmentTween
BorderRadiusTween
BorderTween
BoxConstraintsTween
ColorTween
ConstantTween
DecorationTween
EdgeInsetsGeometryTween
EdgeInsetsTween
FractionalOffsetTween
IntTween
MaterialPointArcTween
Matrix4Tween
RectTween
RelativeRectTween
ReverseTween
ShapeBorderTween
SizeTween
StepTween
TextStyleTween
ThemeDataTween
  • 应用5:

原理跟应用1一样,只是将其应用到了Flow组件,通过Flow组件我们可以自定义组件的布局。在FlowDelegate的paintChildren方法中结合controller的value值动态更新每个子组件的位置(缩放/角度等)