显式动画——Transition系列(配合AnimationController)

2.1 循环旋转——RotationTransition

​ 我们需要使用RotationTransition配合AnimationController实现动画的手动控制,以实现隐式动画(Animated系列)无法完成的功能:循环重播、随时中断、多方协调等。

​ 另外,要使用AnimationController,我们必须使用StatefulWidget,以使用生命周期函数initState()dispose()

​ 我们先简单地看一个实例,在后文中,我们将详细解释其中的内容。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyApp({Key? key}) : super(key: key);
  @override
  _MyAppState createState() => _MyAppState();
}

//SingleTickerProviderStateMixin用于垂直同步
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool _loading = false;

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

  @override
  void dispose() {
    _controller.dispose(); //用完删除,否则内存泄漏
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: Center(
          child: Container(
        width: 300,
        height: 120,
        color: Colors.blue,
        child: RotationTransition(
          turns: _controller,   //这里直接使用controller即可,后文来解释
          //这里的动画还没有开始,调用_controller.forward()开始
          child: const Icon(Icons.repeat,size: 100),
        ),
      )),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {		//按一下按钮开始旋转,再按一下停止。
          _loading ? _controller.stop() : _controller.repeat();
          _loading = !_loading;
        },
      ),
    ));
  }
}

完成效果如下:

IMG_0848

2.2 AnimationController是什么

class AnimationController extends Animation<double>

​ 从本质上来说,AnimationController是一个Animation<double>,正如上面看到的那样,RotationTransition的turns属性本质上要求一个Animation。

​ AnimationController会在Duration时间内,生成0~1的double,如果要更改区间,使用lowerBound和upperBound属性。

​ 示例代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

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

//SingleTickerProviderStateMixin用于垂直同步,屏幕每次刷新,产生一个tick
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {

  /* class AnimationController extends Animation<double> */
  late AnimationController _controller;
  bool _loading = false;

  @override
  void initState() {
    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      lowerBound: 0.0, //与默认一致,生成0-1
      upperBound: 1.0,
      vsync: this,
    )..addListener(() { //dart语法:“..”表示返回值还是前面的表达式
      // _controller.value 在1s内自动生成0~1之间的数字(1秒60帧)
      print("${_controller.value}");
    });

    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose(); //用完删除,否则内存泄漏
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: Center(
          child: Container(
        width: 300,
        height: 120,
        color: Colors.blue,
        //例如:RotateTransition、FadeTransition、ScaleTransition
        child: FadeTransition(
          opacity: _controller, //这里本质上要求一个Animation<double>
          child: const Icon(Icons.repeat, size: 100),
        ),
      )),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          _loading ? _controller.stop() : _controller.repeat(reverse: true);
          _loading = !_loading;
        },
      ),
    ));
  }
}

2.3 控制器补间(Tween)和曲线

​ 我们可以使用drive函数将Animation的值映射到Tween上,也就是说,最终生成的值是在0.5到1.0之间,而不是原来的0.0~1.0了。

ScaleTransition(
  scale: _controller.drive(Tween(begin: 0.5, end: 1.0)),
  ...
),

​ 又比如说,

SlideTransition(
  position: _controller.drive(Tween(begin: Offset.zero, end: const Offset(0.1,0))),
  ...
),

//等价于
SlideTransition(
  position: Tween(begin: Offset.zero, end: const Offset(0.1,0)).animate(_controller),
  ...
),

​ 通过使用chain函数,Tween之间可以叠加,比如,

SlideTransition(
  position: Tween(begin: Offset.zero, end: const Offset(0,1.0))
      .chain(CurveTween(curve: Curves.elasticOut))  //叠加曲线
      .chain(CurveTween(curve: const Interval(0.5, 1.0))) 
      //使用Interval,限制动画播放时间,从 50% 时间开始到 100% ,其他时间不做任何事
      .animate(_controller),
  ...
),

​ 整体代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

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

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late bool _stop = false;

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: Center(
          child: SlideTransition(
              position: Tween(begin: Offset.zero, end: const Offset(0,1.0))
                .chain(CurveTween(curve: Curves.elasticOut))  //叠加曲线
                .chain(CurveTween(curve: const Interval(0.5, 1.0))) 
                      //使用Interval,限制动画播放时间,从 50% 时间开始到 100% ,其他时间不做任何事
                .animate(_controller),
              child: Container(
                width: 300,
                height: 120,
                color: Colors.blue,
              ))),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          _stop ? _controller.repeat() : _controller.reset();
          _stop = !_stop;
        },
      ),
    ));
  }
}

2.4 交错动画——Interval

​ 如上文,使用Interval可以限制播放时间,如此一来,作如下安排,达到五个组件间到交错动画:

  1. 时间0.0~0.2运行第一个动画

  2. 时间0.2~0.4运行第二个动画

  3. 时间0.4~0.6运行第三个动画

  4. 时间0.6~0.8运行第四个动画

  5. 时间0.8~1.0运行第五个动画

    ​ 使用Column控件将以上五个SlideTransition组织起来,并将它们绑定到同一个_controller即可(SlideTransition本身也是Widget):

    class SlideTransition extends AnimatedWidget { ... }
    

2.5 自定义动画——AnimatedBuilder

​ 通过自己动手制作FadeTransition,我们可以看出Transition系列控件的内部原理。同时也可以看到,就像TweenAnimationBuilder那样,使用AnimatedBuilder时我们也可以对动画进行优化,避免反复渲染那些与动画无关的Widget:

//如何使用AnimatedBuilder制作出一个FadeTransition?
child: AnimatedBuilder(
  //每当animtion(_controller)的值变化,flutter就会重新调用builder函数
  animation: _controller, 
  //还记得builder就是复杂版的child吗?
  builder: (context, child) {
    return Opacity(
      opacity: _controller.value,//这里可以用Tween制作补间动画
      child: child
    );
  },
  //下面的child是用于优化
  child: const Icon(Icons.repeat, size: 100)
),
//这是内置的FadeTransition,使用起来更加简单,但是没有办法进行细致的绘制优化
child: FadeTransition(
  opacity: _controller, //这里本质上要求一个Animation<double>
  child: const Icon(Icons.repeat, size: 100),
),

​ 整体代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

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

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    final Animation opacityAnimation = Tween(begin: 0.5, end: 0.8).animate(_controller);
    return MaterialApp(
        home: Scaffold(
      body: Center(
          child: Container(
              width: 300,
              height: 300,
              color: Colors.red,
              child: AnimatedBuilder(
                  animation: _controller, //每当animtion(_controller)的值变化,flutter就会重新调用builder函数
                  builder: (context, child) { //还记得builder就是复杂版的child吗?
                    return Opacity(
                      opacity: opacityAnimation.value,
                      //使用Tween制作补间
                      //需要注意的是这里的opacity是普通变量,所以需要使用Animation.value
                      child: child
                    );
                  },
                  //下面的child是用于优化
                  child: const Icon(Icons.repeat, size: 100)
              ))),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {},
      ),
    ));
  }
}

2.6 使用多个Controller的例子——TickerProviderStateMixin

​ 有时我们会希望使用多个Controller(绝大多数情况下一个足矣),此时我们需要使用TickerProviderStateMixin

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

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

class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
  late AnimationController _expansionController;
  late AnimationController _opacityController;

  @override
  void initState() {
    _expansionController = AnimationController(vsync: this);
    _opacityController = AnimationController(vsync: this);
    super.initState();
  }

  @override
  void dispose() {
    _expansionController.dispose();
    _opacityController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final Animation gradientAnimation =
        Tween(begin: 0.3, end: 1.0).animate(_expansionController);
    return MaterialApp(
        home: Scaffold(
      body: Center(
          child: FadeTransition(
              opacity: _opacityController.drive(Tween(begin: 1.0, end: 0.5)),
              child: AnimatedBuilder(
                animation: _expansionController,
                builder: (context, child) => Container(
                  width: 200,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.white,
                    gradient: RadialGradient(colors: [
                      Colors.blue.shade500,
                      Colors.grey.shade50
                    ], stops: [
                      gradientAnimation.value,
                      gradientAnimation.value + 0.2
                    ]),
                  ),
                ),
              ))),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.badge),
        onPressed: () async {
          _expansionController.duration = const Duration(seconds: 3);
          _expansionController.forward();
          await Future.delayed(const Duration(seconds: 3));
          _opacityController.duration = const Duration(seconds: 4);
          _opacityController.forward();
          await Future.delayed(const Duration(seconds: 4));
          _expansionController.duration = const Duration(seconds: 1);
          _expansionController.reverse();
        },
      ),
    ));
  }
}