Flutter官方文档动画示例小结

1,992 阅读6分钟

目录

  • 基础动画概念和动画类
  • 原始无动画部件
  • 动画示例1(从小变大过渡)
  • 动画示例2(使用 AnimatedWidget )
  • 动画示例3(无限循环动画)
  • 动画示例4(分离部件与动画)
  • 动画示例5(非线性动画)

基础动画概念和动画类

  • Animation Flutter动画库的核心类,任何动画都基于Animation
  • Animation对象可以知道当前动画的状态(value值),不过屏幕上显示的效果是一概不知的,即Animation对象只负责动画过程中的数值变化
  • AnimationController管理动画,例如播放、反转,停用等
  • CurvedAnimation用于实现非线性动画,如快进慢出,慢进快出
  • Tween区间过渡插值,如数字从0到255,动画从黄到绿,分别可以用IntTweenColorTween

原始无动画部件

下面是一个没有动画,宽高固定为300的部件(Widget)

import 'package:flutter/material.dart';

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

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        // 宽高固定
        height: 300,
        width: 300,
        child: FlutterLogo(),
      ),
    );
  }
}

动画示例1(从小变大过渡)

实现图片从小到大(从宽高为0到宽高为300),步骤如下:

  1. 引入动画库import 'package:flutter/animation.dart';,并混入SingleTickerProviderStateMixin
  2. 创建动画控制类,此处可以定义动画的时长
  3. 利用Tween创建动画类,令值在0~300之间变化
  4. 利用addListener方法调用setState更新value
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';

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

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  // 定义动画变量
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    // 实例化控制类,时长3秒, vsync可以在当前动画对用户不可见时节省机器资源(不进行动画计算)
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // 在0~300之间变动
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        // 通过setState令动画中的value产生变化
        setState(() {});
      });

    // 开始播放动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        // setState时value的变化产生在这里
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
    // 注销动画实例,释放内存
    controller.dispose();
    super.dispose();
  }
}

动画示例2(使用 AnimatedWidget )

使用AnimatedWidget组件简化示例1的代码,不再需要手动调用setState,步骤如下:

  1. 使用AnimatedWidget创建动画部件
  2. 在构造参数中接收Animation参数,并传递给父构造函数的listenable参数
  3. build中通过listenable获得动画中的value
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';

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

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // 不再需要手动setState
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(
        animation: animation,
      );

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

class AnimatedLogo extends AnimatedWidget {
  // 构造参数中的第二个参数为Animation,传递给父构造函数的listenable
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    // 通过listenable获取value
    final Animation<double> animation = listenable;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

动画示例3(无限循环动画)

监控动画状态进度,并在过程中根据状态改变动画,实现动画的无限循环,步骤如下:

  1. 增加状态监听addStatusListener
  2. 在监听到动画状态为completed(播放完毕)时,调用reverse()方法反转动画,使动画回到初始状态
  3. 动画回到初始状态后停止时,状态为dismissed,此时再调用forward()方法继续播放,实现无限循环
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';

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

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    /***
     * 通过addStatusListener获得当前动画进度,实现无限循环的动画
     *
     * AnimationStatus的四个状态
     * 1. AnimationStatus.forward   动画播放
     * 2. AnimationStatus.completed 动画播放完成
     * 3. AnimationStatus.dismissed 动画回到初始状态后停止
     * 4. AnimationStatus.reverse   动画反转
     */
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          // 动画完成后反转
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          // 反转回初始状态时继续播放,实现无限循环
          controller.forward();
        }
      });
    // 播放动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(
        animation: animation,
      );

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

class AnimatedLogo extends AnimatedWidget {
  // 构造参数中的第二个参数为Animation,传递给父构造函数的listenable
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    // 通过listenable获取value
    final Animation<double> animation = listenable;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

动画示例4(分离部件与动画)

在前面的动画中可以看到,部件显示的内容和动画混合在一起了,Flutter官网给出了一个可以使动画和部件内容分离的写法 按我理解,所有只是改变宽高(仅当前示例)的内容部件都可以使用这个GrowTransition动画过渡部件 步骤如下:

  1. 将部件内容创建为一个单独的内容部件
  2. 将过渡动画也创建为一个单独的部件,并使用AnimatedBuilder编写过渡的部分
  3. 将动画部件和内容部件组合在一起
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';

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

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    // 创建动画
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  // 将内容部件和动画组合起来
  @override
  Widget build(BuildContext context) => GrowTransition(
        animation: animation,
        child: LogoWidget(),
      );

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

// 内容部件
class LogoWidget extends StatelessWidget {
  Widget build(BuildContext context) => Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        child: FlutterLogo(),
      );
}

// 动画过渡部件
class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  // 需要有动画过渡效果的部件
  final Widget child;
  // 为部件使用的动画
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        /**
         * AnimatedBuilder将宽高定义为过渡状态
         * 按我理解,所有只是改变宽高的内容部件都可以使用这个GrowTransition动画过渡部件
         * 当然也可以定义一个只改变透明度的动画过渡部件复用
         */
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Container(
                  height: animation.value,
                  width: animation.value,
                  child: child,
                ),
            child: child),
      );
}

动画示例5(非线性动画)

非线性动画很好理解,就慢进快出,快进慢出等。使用CurvedAnimation类,步骤如下:

  1. 创建AnimationController,定义相关参数
  2. 创建CurvedAnimation,是得其parent参数为前一步创建的动画控制器,参数curve为动画效果
  3. 创建Tween过渡值,调用evaluate()方法,参数即为上一步创建的CurvedAnimation对象
  4. 接着就调用forward()播放动画即可
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';

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

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // 曲线动画的核心的代码
    animation = CurvedAnimation(parent: controller, curve: Curves.easeOut);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(
        animation: animation,
      );

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

class AnimatedLogo extends AnimatedWidget {
  // 曲线动画使用的过渡效果
  static final _sizeTween = Tween<double>(begin: 0, end: 300);

  // 构造参数中的第二个参数为Animation,传递给父构造函数的listenable
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    // 通过listenable获取value
    final Animation<double> animation = listenable;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        // 曲线动画部分开始
        height: _sizeTween.evaluate(animation),
        width: _sizeTween.evaluate(animation),
        // 曲线动画部分结束
        child: FlutterLogo(),
      ),
    );
  }
}

补充,另一个简单示例(修改动画示例1即可)

    /**
     *  非曲线动画的的另一个示例
     *  把animate的参数从controller改成animate即可
     */
    final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
    animation = Tween<double>(begin: 0, end: 300).animate(curve);
    controller.forward();

参考

Flutter Animations