Ray 的记录站

日常开发实践记录

0%

Flutter 动画 API 简介(Animation API)

本文的主要目的是介绍 Flutter 中的动画 API, 相关概念, 类, 以及方法.

Flutter 中的动画主要分为两大类: 插值动画和物理动画.

  • 插值动画: 指的是通过设置一个起点和终点, 通过提供的变化曲线进行中间帧插值的一种动画方法.
  • 物理动画: 指的是以模拟的方式

为了更好地在 Flutter 中实现动画, 先来看 Flutter 中的常用动画实现模式.

常用动画实现模式

下面是一些常用的动画模式(套路), 便于在日常开发中查阅.

List 或 Grid 动画

这个模式主要用于: List 或 Grid 元素的增删, 详见官方文档.

共享元素的迁移动画

这个模式主要用于: 用户从一个页面选择某元素, 然后将该元素动画迁移到另外一个页面.

在 Flutter 中这样的动画非常容易实现, 使用 Hero Widget 即可. 另外在官方的文档 中可以找到相关的例子. 另外 Gallery 的 Shrine 里面也有相关的例子.

交错动画

即动画以组合的方式呈现, 类似 iOS 中的 KeyFrame(关键帧)动画.

Flutter 的动画 API 概述

Flutter 中动画系统基于 Animation 对象. Animation 表示一个随动画生命期改变的值, 通过值的变化来映射到 UI 属性的变化. 一般情况下, 可动画的 Widget 会接收一个 Animation 作为参数, 然后自动读取当前值来进行动画.

Animation 对象上可以使用 addListener 添加观察者. 一个非常常用的模式是: State 通过观察 animation 来调用 setState 从而驱动重建.

而这个模式的封装有两个: AnimatedWidgetAnimatedBuilder.

AnimatedWidget 用法: 继承它并重写 build 即可. 且框架中封装了许多常用的动画 Widget, 比如 SlideTransition 就是继承自 AnimatedWidget 的.

AnimatedBuilder 用法: 用在更复杂的场景下, 传入 builder 函数.

可以通过 addStatusListener 添加 Animation 的生命期观察者. 动画的生命期一般情况下从 dismissed 开始, 然后是 forward(若值从0到1) 或 reverse(若值从1到0), 然后到结束状态 completed.

AnimationController

要创建动画, 一般都是先创建 AnimationController. 有了它之后, 就可以基于它创建更多动画

Tween

插值器, 它可以对任意值计算从 0 到 1 的插值(或者说将 0 到 1 映射为任意的类型值). 比如颜色(使用 ColorTween), 矩形框(RectTween)等, 这些类都是继承后重写 lerp 方法实现插值, 要写自定义的类也非常方便.

从源码角度看 Flutter 动画实现原理

接收到引擎的 begin frame 消息后, SchedulerBinding 会执行所有通过 scheduleFrameCallback 注册的帧回调(Transient 回调), 而这些回调中就包含动画注册的回调.

由动画关联的 Ticker 使用 scheduleFrameCallback 来注册回调, 每次 tick 都会注册一次. 这样就保证每帧的内容都不一样, 从而形成动画. 在 Ticker 内部的执行流程如下:

start -> scheduleTick(保证只执行一次) -> 注册回调 _tick -> 在 _tick 中调用 _onTick 并再次 scheduleTick.

这样循环往复, 直到 stop 或其他终止条件. 其中 _onTick 是外部传入的, 在源码中可以发现, 源码中只有 TickerProvider 及其子类在构造 Ticker, 且通过 createTicker 方法在构造, 构造 Ticker 时传入 onTick 方法, 对应 _onTick.

createTicker 方法的调用者只有 AnimationController, 通过 vsync.createTicker(_tick); 进行. 这里传入的 _tick 方法就是整个动画控制的核心, 它会在动画时间结束后停止 ticker, 并修改动画状态, 同时通知动画观察者和动画状态观察者.

所以可知, 动画在使用时: 需要 TickerProvider, 通过它再创建 AnimationController 通过控制器来控制动画的整个流程, 另外最重要的是 AnimationController 也是 Animation.

1
2
3
4
class AnimationController extends Animation<double>
with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
// ...
}

Animatable 的绝大部分子类都是 Tween 族的, 意味着 Tween 可以进行链接, 先应用父的映射, 再应用子的映射, 从而可以实现复杂动画.

Animation 也有类似的父子关系, 但含义有区别: 父子关系意味着孩子是被父驱动的.

动画实践

通过 AnimationController 控制动画, 通过 Curve 提供动画曲线, 通过 Tween 插值.

实现自己的曲线也非常简单:

1
2
3
4
5
6
import 'dart:math';

class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}

使用 Tween 的时候, 通过它的 animate 传入一个 Animation 作为入口, 并获取结果 Animation. 一般情况下, 传入的都是 AnimationController, 并且可以在它上面链接多个 Animation, 如下所示:

1
2
3
4
5
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

调用 Tweenanimate 方法处理后, 从 Animation 拿的 value 就是通过 cureve 再次进行映射计算后的值.

一般来说, 使用插值器的场景是在非 0 到 1 的范围内进行线性/非线性动画的情况下.

最基本的动画套路: 通过 Animation 对象提供动画的当前值, 然后添加观察者, 在观察回调中调用 setState 触发重建. 从而形成动画.

Hero 动画

这种动画表示将一个 Widget 从一个页面传递到另外一个页面的动画. 使用 Hero Widget 即可实现.

有两种 Hero 动画方式: 标准的和径向变化的.

在 API 的使用上, 实际是把两个 Hero Widget 放到两个页面内, 它们拥有相同的 Tag, 从而实现转变动画. 当页面入栈和出栈时, 就会触发 Hero 动画. 在动画过程中, Hero Widget 被转移到一个单独的覆盖层中, 所以它不会被任何页面遮住.

在目前的实现中, 还无法将 Hero 动画到一个 Dialog 中, 不过有解决方案, 详见GitHub issue.

使用 Hero 时, 有可能出现找不到 Material 的情况, 所以此时加一层 Material 包裹即可.

径向变形的 Hero 动画则是在飞行的同时对形状进行改变, 主要通过给 Hero 的 createRectTween 赋值插值器生成函数完成, 因为如果不设置这个插值器的话, 默认使用的是线性变化的插值器.

动画 API 的使用套路总结

根据官方文档摘录.

  1. 手动创建动画控制器, 创建插值器并对控制器加工(使用 animate 方法), 也可以多个插值器串联. 在最终的动画对象上添加观察者调用 setState 触发重建, 重建的时候可以是改变某个状态, 或者是直接将动画的值赋值给某个视图属性.

  2. 继承 AnimatedWidget: 它本身是 StatefulWidget, 在构造时传入动画或 changenotifier. 另外框架中有许多这样的 Widget 都是继承自 AnimatedWidget.

  3. 使用 AnimatedBuilder 封装需要动画的 Widget, 并把不需要动画改变属性的子树从动画 Widget 中隔离, 同时分离动画对象的创建.

  4. 使用 ImplicitlyAnimatedWidget 的子类进行隐式动画.

参考文档