动画基本结构

在Flutter中我们可以通过多种方式来实现动画,下面通过一个图片逐渐放大示例的不同实现来演示Flutter中动画的不同实现方式的区别

基础版本

下面我们演示一下最基础的动画实现方式:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '基础版本',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("基础版本")),
  14. body: ScaleAnimationRoute(),
  15. ));
  16. }
  17. }
  18. class ScaleAnimationRoute extends StatefulWidget {
  19. @override
  20. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  21. }
  22. //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
  23. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
  24. with SingleTickerProviderStateMixin {
  25. AnimationController _controller;
  26. Animation<double> _animation;
  27. @override
  28. initState() {
  29. super.initState();
  30. _controller = new AnimationController(
  31. duration: const Duration(seconds: 3), vsync: this);
  32. _animation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
  33. //图片宽高从0变到300
  34. _animation = new Tween(begin: 300.0, end: 3.0).animate(_animation)
  35. ..addListener(() {
  36. setState(() => {});
  37. });
  38. //启动动画(正向执行)
  39. _controller.forward();
  40. }
  41. @override
  42. Widget build(BuildContext context) {
  43. return new Center(
  44. child: Image.asset("images/avatar.jpg",
  45. width: _animation.value, height: _animation.value),
  46. );
  47. }
  48. @override
  49. void dispose() {
  50. //路由销毁时需要释放动画资源
  51. _controller.dispose();
  52. super.dispose();
  53. }
  54. }

上面代码中addListener()函数调用了setState(),所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中,改变Image的宽高,因为它的高度和宽度现在使用的是animation.value ,所以就会逐渐放大。值得注意的是动画完成时要释放控制器(调用dispose()方法)以防止内存泄漏。

上面的例子中并没有指定Curve,所以放大的过程是线性的(匀速),下面我们指定一个Curve,来实现一个类似于弹簧效果的动画过程,我们只需要将initState中的代码改为下面这样即可:

  1. @override
  2. initState() {
  3. super.initState();
  4. _controller = new AnimationController(
  5. duration: const Duration(seconds: 3), vsync: this);
  6. _animation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
  7. //图片宽高从0变到300
  8. _animation = new Tween(begin: 300.0, end: 3.0).animate(_animation)
  9. ..addListener(() {
  10. setState(() => {});
  11. });
  12. //启动动画(正向执行)
  13. _controller.forward();
  14. }

上面代码执行后截取了其中的两帧,效果如图9-1、9-2所示:
动画基本结构及状态监听 - 图1动画基本结构及状态监听 - 图2

使用AnimatedWidget简化版

细心的读者可能已经发现上面示例中通过addListener()和setState()来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,并允许我们将widget分离出来,重构后的代码如下:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '使用AnimatedWidget简化版',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("使用AnimatedWidget简化版")),
  14. body: ScaleAnimationRoute1(),
  15. ));
  16. }
  17. }
  18. class AnimatedImageTest extends AnimatedWidget {
  19. final Animation<double> animation;
  20. // AnimatedImageTest({Key key, Animation<double> animation})
  21. // : super(key: key, listenable: animation);
  22. AnimatedImageTest({Key key, this.animation})
  23. : super(key: key, listenable: animation);
  24. Widget build(BuildContext context) {
  25. // final Animation<double> animation = animation;
  26. return new Center(
  27. child: Image.asset("images/avatar.jpg",
  28. width: animation.value, height: animation.value),
  29. );
  30. }
  31. }
  32. class ScaleAnimationRoute1 extends StatefulWidget {
  33. @override
  34. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  35. }
  36. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
  37. with SingleTickerProviderStateMixin {
  38. Animation<double> animation;
  39. AnimationController controller;
  40. initState() {
  41. super.initState();
  42. controller = new AnimationController(
  43. duration: const Duration(seconds: 3), vsync: this);
  44. //图片宽高从0变到300
  45. animation = CurvedAnimation(parent: controller, curve: Curves.bounceInOut);
  46. animation = new Tween(begin: 0.0, end: 300.0).animate(animation);
  47. //启动动画
  48. controller.forward();
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. return AnimatedImageTest(
  53. animation: animation,
  54. );
  55. }
  56. dispose() {
  57. //路由销毁时需要释放动画资源
  58. controller.dispose();
  59. super.dispose();
  60. }
  61. }

用AnimatedBuilder重构版

用AnimatedWidget可以从动画中分离出widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget中
假设如果我们再添加一个widget透明度变化的动画,那么我们需要再实现一个AnimatedWidget,这样不是很优雅,如果我们能把渲染过程也抽象出来,那就会好很多,而AnimatedBuilder正是将渲染逻辑分离出来, 上面的build方法中的代码可以改为:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '使用AnimatedWidget简化版',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("使用AnimatedWidget简化版")),
  14. body: ScaleAnimationRoute1(),
  15. ));
  16. }
  17. }
  18. class AnimatedImageTest extends AnimatedWidget {
  19. final Animation<double> animation;
  20. // AnimatedImageTest({Key key, Animation<double> animation})
  21. // : super(key: key, listenable: animation);
  22. AnimatedImageTest({Key key, this.animation})
  23. : super(key: key, listenable: animation);
  24. Widget build(BuildContext context) {
  25. // final Animation<double> animation = animation;
  26. return new Center(
  27. child: Image.asset("images/avatar.jpg",
  28. width: animation.value, height: animation.value),
  29. );
  30. }
  31. }
  32. class ScaleAnimationRoute1 extends StatefulWidget {
  33. @override
  34. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  35. }
  36. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
  37. with SingleTickerProviderStateMixin {
  38. Animation<double> animation;
  39. AnimationController controller;
  40. initState() {
  41. super.initState();
  42. controller = new AnimationController(
  43. duration: const Duration(seconds: 3), vsync: this);
  44. //图片宽高从0变到300
  45. animation = CurvedAnimation(parent: controller, curve: Curves.bounceInOut);
  46. animation = new Tween(begin: 0.0, end: 300.0).animate(animation);
  47. //启动动画
  48. controller.forward();
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. // return AnimatedImageTest( animation: animation, );
  53. return AnimatedBuilder(
  54. animation: animation,
  55. child: Image.asset("images/avatar.jpg"),
  56. builder: (BuildContext ctx, Widget child) {
  57. return new Center(
  58. child: Container(
  59. height: animation.value,
  60. width: animation.value,
  61. child: child,
  62. ),
  63. );
  64. },
  65. );
  66. }
  67. dispose() {
  68. //路由销毁时需要释放动画资源
  69. controller.dispose();
  70. super.dispose();
  71. }
  72. }

上面的代码中有一个迷惑的问题是,child看起来像被指定了两次。但实际发生的事情是:将外部引用child传递给AnimatedBuilder后,AnimatedBuilder再将其传递给匿名构造器, 然后将该对象用作其子对象。最终的结果是AnimatedBuilder返回的对象插入到widget树中。

对AnimatedBuilder进一步封装

也许你会说这和我们刚开始的示例差不了多少,其实它会带来三个好处:

  1. 不用显式的去添加帧监听器,然后再调用setState()了,这个好处和AnimatedWidget是一样的。
  2. 动画构建的范围缩小了
    • 如果没有builder,setState()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用
    • 而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。
  3. 通过AnimatedBuilder可以封装常见的过渡效果来复用动画。下面我们通过封装一个GrowTransition来说明,它可以对子widget实现放大动画:
    1. class GrowTransition extends StatelessWidget {
    2. GrowTransition({this.child, this.animation});
    3. final Widget child;
    4. final Animation<double> animation;
    5. Widget build(BuildContext context) {
    6. return new Center(
    7. child: new AnimatedBuilder(
    8. animation: animation,
    9. builder: (BuildContext context, Widget child) {
    10. return new Container(
    11. height: animation.value,
    12. width: animation.value,
    13. child: child
    14. );
    15. },
    16. child: child
    17. ),
    18. );
    19. }
    20. }
    这样,最初的示例就可以改为: ```dart import ‘package:flutter/material.dart’;

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

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: true, title: ‘使用AnimatedWidget简化版’, theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar(title: Text(“使用AnimatedWidget简化版”)), body: ScaleAnimationRoute1(), )); } }

class AnimatedImageTest extends AnimatedWidget { final Animation animation;

// AnimatedImageTest({Key key, Animation animation}) // : super(key: key, listenable: animation); AnimatedImageTest({Key key, this.animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { // final Animation animation = animation; return new Center( child: Image.asset(“images/avatar.jpg”, width: animation.value, height: animation.value), ); } }

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

class _ScaleAnimationRouteState extends State with SingleTickerProviderStateMixin { Animation animation; AnimationController controller;

@override initState() { super.initState(); controller = new AnimationController( duration: const Duration(seconds: 3), vsync: this); //图片宽高从0变到300 animation = CurvedAnimation(parent: controller, curve: Curves.bounceInOut); animation = new Tween(begin: 0.0, end: 300.0).animate(animation); //启动动画 controller.forward(); }

@override Widget build(BuildContext context) { // return AnimatedImageTest( animation: animation, ); return GrowTransition( child: Image.asset(“images/avatar.jpg”), animation: animation, ); }

dispose() { //路由销毁时需要释放动画资源 controller.dispose(); super.dispose(); } }

class GrowTransition extends StatelessWidget { GrowTransition({this.child, this.animation}); final Widget child; final Animation 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), ); } }

  1. Flutter中正是通过这种方式封装了很多动画,如:
  2. - FadeTransition
  3. - ScaleTransition
  4. - SizeTransition
  5. 很多时候都可以复用这些预置的过渡类。
  6. <a name="EQjdf"></a>
  7. ##
  8. <a name="AboV8"></a>
  9. ##
  10. <a name="CNPji"></a>
  11. ## 动画状态监听
  12. 上面说过,我们可以通过AnimationaddStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:
  13. | 枚举值 | 含义 |
  14. | --- | --- |
  15. | dismissed | 动画在起始点停止 |
  16. | forward | 动画正在正向执行 |
  17. | reverse | 动画正在反向执行 |
  18. | completed | 动画在终点停止 |
  19. 示例:我们将上面图片放大的示例改为先放大再缩小再放大……这样的循环动画。要实现这种效果,我们只需要监听动画状态的改变即可,即:在动画正向执行结束时反转动画,在动画反向执行结束时再正向执行动画。代码如下:
  20. ```dart
  21. @override
  22. initState() {
  23. super.initState();
  24. controller = new AnimationController(
  25. duration: const Duration(seconds: 3), vsync: this);
  26. //图片宽高从0变到300
  27. animation = CurvedAnimation(parent: controller, curve: Curves.bounceInOut);
  28. animation = new Tween(begin: 0.0, end: 300.0).animate(animation);
  29. //启动动画
  30. controller.forward();
  31. animation.addStatusListener((status) {
  32. if (status == AnimationStatus.completed) {
  33. //动画执行结束时反向执行动画
  34. controller.reverse();
  35. } else if (status == AnimationStatus.dismissed) {
  36. //动画恢复到初始状态时执行动画(正向)
  37. controller.forward();
  38. }
  39. });
  40. //启动动画(正向)
  41. controller.forward();
  42. }