Flutter 掌握动画开发

3,302 阅读7分钟

主本文主要说明动画的基本原理和简单的动画的实例,如有不当之处敬请指正。

阅读本文大约需要 6 分钟

背景

给UI界面设计合理的动画,可以让用户觉得更加流畅、直观,提高用户的交互使用感受,改善用户体验。

在 Flutter 中动画分为两类:基于补间 (Tween) 的和基于物理 (Physics) 的;

补间动画是介于两者之间的简称,在补间动画中定义起点和终点、时间点以及定义时间变化和速度的曲线,然后由系统计算如何从开始点到结束点。

物理动画是运动被模拟为与真实世界的行为相似,比如抛一件物体,它落在什么地方取决于这个物体的重量,抛出去的速度以及这个物体与地面的高度,类似数学中的抛物线运动轨迹。

介绍

在 Flutter 中想要实现动画效果离不开几个核心的角色:Animation(动画对象),AnimationController(动画控制器),Tweens(插值器),Curves(动画曲线);

1、Animation

在 Flutter 中动画本身和UI渲染没有任何关系,Animation是一个抽象类,它拥有其当前值和状态(完成或停止),Flutter 中的动画系统就是基于 Animation 对象的。其中比较常用的就是Animation类是Animation。它可以通过其 value 属性来获取当前动画的值。

Animation 除了可以生成 double 的值之外还可以生成如:颜色--Animation<Color> 或者大小--Animation<Size>

Animation 对象可以拥有 Listeners 和 StatusListeners 监听器,可以用 addListener()addStatusListener() 来添加。只要动画的的值发生变化,就会调用监听器。正常我们在 Listeners 中调用setState() 来触发UI重建;动画开始、结束、向前移动或向后移动时会调用StatusListener。

2、AnimationController

AnimationController 是一个特殊的 Animation 对象,在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController 会在特定的时间内线性的生成0.0到1.0的数字。AnimationController派生于 Animation<double>,因此可以在需要Animation对象的任何地方使用。不但如此,AnimationController还具有控制动画的其他方法,比如 forward()方法可以启动动画。

AnimationController({
    double value,
    this.duration,
    this.reverseDuration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  })

创建 AnimationController 必须需传入 vsync,传入 vsunc 是为了防止动画的UI不在当前屏幕时,不需要绘制,从而防止消耗不必要的资源。通过将 SingleTickerProviderStateMixin 混入到类定义中,就可以将 statefu l对象作为 vsync 的值。

除了 vsync 还可以传入正向动画执行的时间 duration 以及反向动画执行时间 reverseDuration 等。

常用函数:

序号 方法 介绍
1 forward() 开始播放动画
2 stop() 停止动画
3 reset() 重制动画
4 reverse() 反向播放动画,必须处于正向动画播放完成的状态之后才有用
5 dispose() 释放动画占用资源
6 repeat() 循环播放动画

注意:动画完成时释放控制器(调用 dispose() 方法)以防止内存泄漏

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

3、Tween

默认情况下,AnimationController对象的范围从0.0到1.0。如果您需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。比如,可以生产从0-100的数字:

final Tween doubleTween = new Tween<double>(begin: 0.0, end: 100.0);

Tween是一个无状态(stateless)对象,继承自Animatable<T>,而不是继承自 Animation<T>。Tween 需要两个值,分别是:begin 和 end。Tween的唯一职责就是定义从输入范围到输出范围的映射。

Animatable与Animation相似,不是必须输出double值,也可以是颜色,比如,从白色到黑色:

final Tween colorTween = new ColorTween(begin: Colors.withe, end: Colors.black);

Tween 可以通过 animate() 方法传入 controller 对象创建 Animation 对象。如下

AnimationController _animationController = AnimationController(animationBehavior:AnimationBehavior.normal,vsync: this);
Tween<double> _tween = Tween<double>(begin: 0.0, end: 100.0)..animate(_animationController);

4、CurvedAnimation

Curves 用来调整动画过程中随时间的变化率,默认情况下,动画以均匀的线性模型变化。Flutter 内部也提供了一系列实现相应变化率的 Curves 对象:linea ------ 线性,decelerate ------ 减速等。

当然,也可以自定义继承 Curves 的类来定义动画的变化率,如:

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

5、添加监听

目前为止动画只是实现了自身数值的变化,并没有让 Widget 动起来,这里我们需要对动画数值进行监听,然后使用 setstatus 来更新 Widget 的属性,从而使 Widget 动起来。

添加数值监听:

Animation animation = CurvedAnimation(parent: _animationController, curve: Curves.linear);
    animation.addListener((){
      setState(() {
        
      });
    });

除此之外我们还可以监听动画的状态变更,当动画结束时我们反转动画,当动画的反转也结束后我们从新开始动画,这样动画就会一直这样循环下去。

状态变更监听:

animation.addStatusListener((status){
      print(status);
    });

6、AnimatedWidget

AnimatedWidget 类允许您从 setState() 调用中的动画代码中分离出 widget 代码。AnimatedWidget 不需要维护一个 State 对象来保存动画。

以下代码为官方文档自定义 AnimatedLogo

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

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

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

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

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

AnimatedWidget 为什么不需要维护一个 State 对象来保存动画呢?

AnimatedWidget 源码中看一看出 AnimatedWidget 是继承自 StatefulWidget 类,在 AnimatedWidget 中,创建 state 是创建了 _AnimatedState,接着看 _AnimatedState 类部分源码:

abstract class AnimatedWidget extends StatefulWidget{
  
   @override
  _AnimatedState createState() => _AnimatedState();
  
}

_AnimatedState 类的 initState 方法添加了监听 _handleChange,并在 didUpdateWidgetdispose 方法中移除了,_handleChange 里面只有一行代码就是 setState 方法:

_AnimatedState 源码

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    // 添加监听
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 移除
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    // 移除
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

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

  @override
  Widget build(BuildContext context) => widget.build(context);
}

7、并行动画

所谓的并行动画就是一起执行多个动画,在 Flutter 中可以在同一个动画控制器上使用多个Tween,然后每个Tween管理动画中的不同效果,从而实现多个动画同时执行。

final AnimationController controller =
    new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
final Animation<double> sizeAnimation =
    new Tween(begin: 0.0, end: 300.0).animate(controller);
final Animation<double> opacityAnimation =
    new Tween(begin: 0.1, end: 1.0).animate(controller);

可以通过sizeAnimation.value来获取大小,通过opacityAnimation.value来获取不透明度,但AnimatedWidget的构造函数只接受一个动画对象。 为了解决这个问题,可以创建了自己的Tween对象并显式计算了这些值。

build方法.evaluate()在父级的动画对象上调用Tween函数以计算所需的sizeopacity值。

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

class AnimatedLogo extends AnimatedWidget {
  // The Tweens are static because they don't change.
  static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
  static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);

  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: new Container(
          margin: new EdgeInsets.symmetric(vertical: 10.0),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: new FlutterLogo(),
        ),
      ),
    );
  }
}

实例

效果图

1、缩放动画

直接贴代码

///放大缩小动画
  Widget scale() {
    return Column(
      children: <Widget>[
        Container(
          height: 170,
          child: Center(
            child: Container(
              width: _scaleAnimation.value,
              height: _scaleAnimation.value,
              child: new FlutterLogo(),
            ),
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              color: Colors.blue,
              child: Text(
                "放大",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _scaleController.forward();
              },
            ),
            RaisedButton(
              color: Colors.red,
              child: Text(
                "缩小",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _scaleController.reverse();
              },
            )
          ],
        ),
      ],
    );
  }

2、淡入淡出动画

代码:

/// 淡入淡出
  Widget alpha() {
    return Column(
      children: <Widget>[
        Container(
          height: 170,
          child: Center(
            child: Container(
              height: 100,
              width: 100,
              child: Opacity(
                opacity: _alphaAnimation.value,
                child: FlutterLogo(),
              ),
            ),
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              color: Colors.blue,
              child: Text(
                "淡入",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _alphaController.forward();
              },
            ),
            RaisedButton(
              color: Colors.red,
              child: Text(
                "淡出",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _alphaController.reverse();
              },
            )
          ],
        ),
      ],
    );
  }

注意,一个 Widget 使用多个animationController 需要修改混入SingleTickerProviderStateMixin 为 TickerProviderStateMixin。

结尾

完整代码奉上GitHub地址:fluter_demo ,欢迎star和fork。

到此,本文就结束了,如有不当之处敬请指正,一起学习探讨,谢谢🙏。