Flutter 动画

1,322 阅读7分钟

每个平台对于动画的实现大同小异,手段大部分都是在60帧(部分Android机型90FPS,部分iPad是120FPS)的刷新频率下实现UI的多次变化,利用人眼视觉残留实现“流畅”的动作。

  • 16 FPS: 比较流畅
  • 32 FPS: 非常的细腻平滑
  • 大于32 FPS: 人眼感受无差别

在理想状态下,Flutter 能够实现 60 FPS。(这里的刷新频率是否跟随硬件,找到资料再更新)

但是每种UI框架对动画的抽象方式都不一样,而 Flutter 中实现一个动画需要涉及到 Animation、Curve、Controller、Tween这四个角色。

动画分解

Animation

Animation是一个抽象类,从下面的Animation的部分源码可以看出,class Animation 仅定义了动画当前的值和状态,以及监听方法等内容,与UI展示样式等相关定义和属性无关(color, width 等)。

abstract class Animation<T> extends Listenable implements ValueListenable<T> {
  const Animation();
  @override
  void addListener(VoidCallback listener);
  @override
  void removeListener(VoidCallback listener);
  void addStatusListener(AnimationStatusListener listener);
  void removeStatusListener(AnimationStatusListener listener);
  AnimationStatus get status;
  @override
  T get value;
  bool get isDismissed => status == AnimationStatus.dismissed;
  bool get isCompleted => status == AnimationStatus.completed;
  // 代码省略
  // ...
}

AnimationController

继承自 abstract class Animation 类,用于控制动画的 进行(forward)、停止(stop)、反向播放(reverse)。

默认情况下,AnimationController 对象会在动画的每一帧,按照动画曲线的规律生成 0 - 1 区间内的值。如下代码:

final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);

这段代码的意义是:在2000毫秒(秒)内,随着帧刷新的频率,线性的生成 0 - 1 区间内的值。

其中 vsync 参数需要传入 TickerProvider 对象,用于屏幕刷新时的回调。

传入 TickerProvider 对象除了提供屏幕刷新回调以外,还可以防止 屏外动画的问题出现。当动画UI不在当前屏幕时,或者手机锁屏时,动画刷新会停止,避免消耗不必要的资源。

当然,动画如此常用的操作,Flutter绝不会让你花费很大的代价去实现,通常我们只要在 State 中 mixin SingleTickerProviderStateMixin 即可。 如下代码:

class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
    //...
}

动画的实现通常伴随着 AnimationControlelr 对象的 dispose,势必需要借助 StatefullWidget 的声明周期方法dispose。自然也会有对应的State对象,所以这里不需要去纠结 StatelessWidget 是否可以使用 TickerProviderStateMixin 的疑问。

以上所讲的内容均是在 线性动画 下,如果想要执行非线性动画,则需要借助 Curve 的协助。

Curve

动画中使用 Curve 可以改变动画曲线,Curve 已经提供常用的动画曲线,下面列出部分枚举类:

Curves曲线 | 动画过程 - | - linear | 匀速的 decelerate | 匀减速 ease | 开始加速,后面减速 easeIn | 开始慢,后面快 easeOut | 开始快,后面慢 easeInOut | 开始慢,然后加速,最后再减速

除了已经提供好的 Curve 曲线,也可以自定义动画曲线,实现起来也很简单。下面是一个正弦曲线的实现:

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

从上面的代码可以看出,要实现一个自定义动画曲线,只需要重写 Curvetransform 方法即可。

如果要实现的动画效果不满足于0-1的区间的话,还可以借助 Tween 对象实现自定义动画区间。

Tween

提供开发自定义动画区间的能力。如下示例所示:

Tween<double>(begin: -200.0, end: 0.0);
Tween<Color>(begin: Colors.transparent, end: Colors.black54);
Tween<EdgeInsets>(begin: const EdgeInsets.only(left: .0), end: const EdgeInsets.only(left: 100.0)

这些均不是0 - 1 的区间。Color的区间可以实现一个Color到另一个Color的渐变过渡。EdgeInsets可以实现间距的渐变。

如此看起来,貌似Tween就已经可以实现0-1到任意区间的映射了,然而,事情绝不会如此顺利。

让我们来看一下 Tween的定义:

class Tween<T extends dynamic> extends Animatable<T> {
  Tween({ this.begin, this.end });
  T begin;
  T end;

  /// Returns the value this variable has at the given animation clock value.
  ///
  /// The default implementation of this method uses the [+], [-], and [*]
  /// operators on `T`. The [begin] and [end] properties must therefore be
  /// non-null by the time this method is called.
  @protected
  T lerp(double t) {
    assert(begin != null);
    assert(end != null);
    return begin + (end - begin) * t;
  }
  @override
  T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }

  @override
  String toString() => '$runtimeType($begin \u2192 $end)';
}

从上面的源码可以看出,Tween的定义十分的简单,绝不会支持所有类型的区间映射。lerp函数已经告诉我们,类型 T 的对象,需要进行 + - * 三则运算,因此,能够运用 Tween 自动实现区间映射的对象,只能是实现了三则运算的对象,如上面例子中的 doubleEdgeInsets,而Color则不能直接使用Tween实现区间映射。这时候就需要开发者自行实现double -> Color类型的区间映射。

那我们就自己实现一个 ColorTween

class ColorTween extends Tween<Color> {
  ColorTween({ Color begin, Color end }) : super(begin: begin, end: end);
  @override
  Color lerp(double t) => Color.lerp(begin, end, t);
}

我们利用 Color 对象的 lerp 方法很轻松的实现了 ColorTween,只是重写了 Tween 类的 lerp 方法,返回区间映射的计算方法而已。类比ColorTween的实现,其他类型的区间映射,也可以如此写。只要你喜欢,也可以实现正弦函数的区间映射关系。

Flutter 已经提供了一些线程的Tween子类给开发者使用:

  • ColorTween
  • IntTween
  • ReverseTween
  • SizeTween
  • RectTween
  • StepTween
  • ConstantTween

动画的当前值,动画的执行,动画曲线,动画区间 都已经实现了,那下一步就是结合这四个部分,实现动画效果。

完整的动画创建过程

先来看一个完整的动画定义:

// 1. 创建动画控制类 AnimationController,用于执行动画
final AnimationController _controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
// 2. 使用 CurvedAnimation 结合 动画控制类 和 动画曲线类,返回一个 「具有指定动画曲线的」「动画」 对象
fianl CurvedAnimation curvedAnimation = CurvedAnimation(
    parent: _controller,
    curve: Curves.ease,
);
// 3. 自定义区间 再次结合 CurvedAnimation,生成一个「具有指定区间值」和「指定动画曲线」的「动画」对象
final Animation<double> height = Tween<double>(begin: 0, end: 300.0).animate(curvedAnimation);

上述就是一个完整的动画创建过程,其中使用了一个新的 CurvedAnimation 类,从名字就可以看出,CurvedAnimationCurveAnimation都有关系,关系就是结合这两者,生成一个具有指定动画曲线Animation对象。

CurvedAnimation 继承自 Animation<T>,同样,它可以标识动画的当前值,却不能控制动画的执行。

动画已经创建出来了,怎么根据创建好的动画构建出可以刷新的动画呢。

这里有主要的三种方式构建动画UI。有兴趣的可以看这篇文章。这里就直接上手最推荐使用AnimationedBuilder。示例代码如下:

@override
Widget build(BuildContext context) {
  return Center(
    child: AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget _) => Container(
        width: 200,
        height: height.value,
        color: Colors.red,
      ),
    ),
  );
}

完整代码如下:

class BasicAnimation extends StatefulWidget {
  @override
  _BasicAnimationState createState() => _BasicAnimationState();
}

class _BasicAnimationState extends State<BasicAnimation> with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  Animation<double> _height;

  @override
  void initState() {
    _animationController = AnimationController(vsync: this, duration: Duration(seconds: 1));
    _height = CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
    _height = Tween<double>(begin: 0, end: 200).animate(_height);

    super.initState();
    
    _animationController.forward();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
          child: Container(
            width: 300,
            height: 300,
            color: Colors.grey,
            alignment: Alignment.bottomCenter,
            child: _buildAnimatedWidget,
          ),
        ));
  }

  Widget get _buildAnimatedWidget => AnimatedBuilder(
        animation: _animationController,
        builder: (BuildContext context, Widget child) {
          print('build animation');
          return Container(
            width: 40,
            height: _height.value,
            color: Colors.red,
          );
        },
      );

  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }
}

交织动画(组合动画)

类似于组动画,或者动画组的概念。同时或者交叉的执行多个动画。

下面我们实现一个:

  • 前半部分高度从0->300,同时颜色从绿色到红色
  • 后半部分往右平移
  • 动画执行结束之后,反向执行,直到结束

的一个组合动画。

我们直接看代码(未完待续...)

其他动画

  • AnimatedCrossFade
  • 转场动画:Hero

参考内容