动画

Flutter 中的动画分为两类

  • 补间动画:定义了起点、终点、时间轴、过渡时间、速度曲线,然后框架会自动计算如何从起点过渡到终点
  • 基于物理基础的动画:动作是模拟真实世界的行为来进行建模的,如现实世界中的抛物线

Animation

动画系统的首要组成部分为 Animation 类。一个动画表现为可在它的生命周期内发生变化的特定类型的值。大多数需要执行动画的组件都需要接收一个 Animation 对象作为参数,从而能从中获取到动画的当前状态值以及英国监听哪些具体值的更改

addListener

每当动画的状态值发生改变时,动画都会通知所有通过 addListener 添加的监听器。一个正在监听动画的 State 对象会调用自身的 setState 方法,将自身传入这些监听器的回调函数来通知组件需要根据新状态值进行重新构建

有个两个组件可以帮助其他组件在动画改变值时进行重新构建

  • AnimatedWidget:对于无状态动画组件来说是尤其有用。要使用它,只需继承它并实现一个 build 方法
  • AnimationBuilder:对于希望将动画作为复杂组件的 build 方法的其中一部分的情况非常有用。要使用它,只需构造组件并将 AnimationBuilder 传递给组件的 builder 方法

addStatusListener

AnimationStatus 表示动画将如何随时间进行变化。当动画的状态发生变化时,动画都会通知所有通过 addStatusListener 添加的监听器

  • 通常情况下,动画会从 dismissed 状态开始,表示它主语变化区间的开始点
  • 下一状态可能是 forward(从 0 到 1) 或 reverse(从 1 到 0)
  • 最终动画到达其区间的结束点,则会变成 completed 状态

AnimationController

要创建动画,首先要创建一个 AnimationController,除了作为动画本身,还可以用来控制动画

一旦创建了一个动画控制器,就可以基于它来构建其他动画,如

  • 创建一个 ReverseAnimation,效果是复制一个动画,但是将其反向运行
  • 创建一个 CurvedAnimation,效果是用 Curve 来调整动画的值

补间动画

想要在 0 到 1 的区间之外设置动画,可以使用 Tween<T>,它可以在它的 begin 值和 end 值之间进行插值补间。

许多类都有特定的 Tween 子类,它们能提供基于特定类型的插值行为,如

  • ColorTween 可以在颜色间进行差值
  • RectTween 可以在矩形之间进行插值

可以创建自己的 Tween 子类并覆盖其 lerp 方法来定义自己的补间动画

补间动画本身只定义了如何在两个值之间进行插值,要获取动画当前帧的具体值,还需要一个动画来确定当前状态。有两种方法可以将补间动画与动画组合在一起以获得动画的具体值

  • evaluate 方法处理动画的当前值从而得到对应的插值。这种方法对于已经监听动画并因此在动画改变值时重新构建的组件最有效
  • animate 方法处理一个动画。该方法返回一个包含补间动画插值的新的 Animation。这种方法对于当你想要将新创建的动画提供给另一个组件时最有效,它可以直接读取包含补间动画的插值以及监听对应插值的更改

架构

动画实际上是由许多核心模块共同构建的

  • 调度器:SchedulerBinding 是一个暴露出 Flutter 调度原语的单例类。每当一帧需要显示时,引擎会触发一个 开始帧 回调,调度程序会将其多路传输给所有使用 scheduleFrameCallback() 注册的监听器
  • 运行器:Ticker 类挂载在调度器的 scheduleFrameCallback() 的机制上,来达到每次运行都会触发回调的效果
  • 模拟器:Simulation 抽象类将相对时间值(运行时间)映射为双精度值,并且有完成的概念。针对不同的效果,Simulation 类有各种具体实现
  • Animatables 抽象类将双精度值映射为特定类型的值,它是无状态和不可变的。将 Animatable<double>(父类)传递给一个 Animatablechain() 方法会创建一个新的 Animatable 子类,这个子类会先应用父类的映射,然后应用子类的映射
  • 曲线:Curve 抽象类映射范围 0.0-1.0 的双精度值,它是无状态和不可变的

上面的都是些基本概念,与官网上的一样

一个简单的示例

class MyAnimatedWidget extends StatefulWidget {
  
  _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
}

class _MyAnimatedWidgetState extends State<MyAnimatedWidget>
    with SingleTickerProviderStateMixin {
  // 定义动画函数
  late Animation<double> _animation;
  // 定义控制器
  late AnimationController _controller;
  // 定义补间动画,此处只是一个时间范围,需要结合 animate 才能使用(在下面的 scale 属性处)
  // 如果多个属性的补间动画不同,需要分别指定
  final _tween = Tween<double>(begin: 0.9, end: 1.0);

  
  void initState() {
    super.initState();
    // 初始化控制器和动画函数
    _controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    )..repeat(reverse: true);
    _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
  }

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

  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return Transform.scale(
          scale: _tween.animate(_animation).value,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.red,
          ),
        );
      },
    );
  }
}

主动画(Hero)

Hero 指的是在不同页面间飞跃的 widget

可以简单理解为 CSStransition

  • 标准 hero 动画:一个标准 hero 动画使 hero 从一个页面飞到新页面,通常以不同大小到达不同的目的地
  • 径向 hero 动画:在径向 hero 动画中,随着 hero 在页面间飞翔,它的形状也会由圆形变成矩形

Hero 动画需要使用两个 Hero widgets 来完成,一个在原页面中,另一个在目标页面中。Hero 动画代码有如下结构

  • 定义一个起始 Hero widget,称为 source hero。该 hero 指定图形表示(通常是图像),以及识别标签,并且在由原页面定义的当前显示的 widget 树中
  • 定义一个终止 Hero widget,称为 destination hero。该 hero 也指定图形表示,并与 source hero 使用同样的标签(一般来讲,起始和终止几乎有完全一样的 widget
  • 创建一个含有 destination hero 的页面。目标页面定义了动画结束时应有的 widget
  • 通过推送目标页面到 Navigator 堆栈来触发动画。 Navigator 推送并弹出操作触发原页面和目标页面中含有配对标签 heroeshero 动画

通过 tween 来界定 Hero 从起点到终点的界限(插入的大小和位置),并在图层上执行动画

实现原理

简单来讲就是有一个中间层在执行动画,起始和目标页仅存放起始和终止的 widget

交织动画(Staggered)

交织动画是一个简单的概念:视觉变化是随着一系列的动作发生,而不是一次性的动作。动画可能是纯粹顺序的,一个改变随着一个改变发生,动画也可能是部分或者全部重叠的。动画也可能有间隙,没有变化发生

可以简单理解为 CSS 中的 animation,通常使用 AnimationController 来创建,方便控制动画,参照前面的示例

Last Updated:
Contributors: af