交错动画是一个简单的概念:视觉变化以一系列操作发生,而不是一次全部发生。动画可能是纯粹的顺序,一个变化接一个地发生;也可能部分或完全重叠;还可能存在间隙,期间没有变化发生。

本指南将演示如何在 Flutter 中构建交错动画。

以下视频演示了 basic_staggered_animation 执行的动画

在新标签页中观看 YouTube 视频:“交错动画示例”

在视频中,你将看到一个单个组件的以下动画,它最初是一个带有略微圆角的带边框的蓝色方块。该方块按以下顺序进行变化

  1. 淡入
  2. 变宽
  3. 向上移动时变高
  4. 转换为带边框的圆形
  5. 颜色变为橙色

动画正向运行后,会反向运行。

交错动画的基本结构

#

下图显示了 basic_staggered_animation 示例中使用的 Interval。你可能会注意到以下特征

  • 不透明度在时间轴的前 10% 发生变化。
  • 不透明度变化与宽度变化之间存在微小间隙。
  • 在时间轴的最后 25% 没有任何动画。
  • 增加内边距使组件看起来向上升起。
  • 将边框半径增加到 0.5,将带圆角的方块转换为圆形。
  • 内边距和高度变化发生在完全相同的间隔内,但它们不必如此。

Diagram showing the interval specified for each motion

要设置动画

  • 创建一个管理所有 AnimationsAnimationController
  • 为每个正在动画的属性创建一个 Tween
    • Tween 定义了一个值范围。
    • Tweenanimate 方法需要 parent 控制器,并为该属性生成一个 Animation
  • Animationcurve 属性上指定间隔。

当控制动画的值改变时,新动画的值也会改变,从而触发 UI 更新。

以下代码为 width 属性创建了一个补间。它构建了一个 CurvedAnimation,指定了一个缓和曲线。有关其他可用的预定义动画曲线,请参阅 Curves

dart
width = Tween<double>(
  begin: 50.0,
  end: 150.0,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: const Interval(
      0.125,
      0.250,
      curve: Curves.ease,
    ),
  ),
),

beginend 值不一定是双精度浮点数。以下代码使用 BorderRadius.circular() 构建了 borderRadius 属性(控制方块圆角程度)的补间。

dart
borderRadius = BorderRadiusTween(
  begin: BorderRadius.circular(4),
  end: BorderRadius.circular(75),
).animate(
  CurvedAnimation(
    parent: controller,
    curve: const Interval(
      0.375,
      0.500,
      curve: Curves.ease,
    ),
  ),
),

完整的交错动画

#

与所有交互式组件一样,完整的动画由一个组件对组成:一个无状态组件和一个有状态组件。

无状态组件指定 Tweens,定义 Animation 对象,并提供一个 build() 函数,负责构建组件树的动画部分。

有状态组件创建控制器,播放动画,并构建组件树的非动画部分。当在屏幕上检测到点击时,动画开始。

basic_staggered_animation 的 main.dart 完整代码

无状态组件:StaggerAnimation

#

在无状态组件 StaggerAnimation 中,build() 函数实例化了一个 AnimatedBuilder——一个用于构建动画的通用组件。AnimatedBuilder 构建一个组件,并使用 Tweens 的当前值对其进行配置。该示例创建了一个名为 _buildAnimation() 的函数(它执行实际的 UI 更新),并将其分配给其 builder 属性。AnimatedBuilder 侦听来自动画控制器的通知,在值改变时将组件树标记为脏。对于动画的每个计时,值都会更新,从而导致调用 _buildAnimation()

dart
class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({super.key, required this.controller}) :

    // Each animation defined here transforms its value during the subset
    // of the controller's duration defined by the animation's interval.
    // For example the opacity animation transforms its value during
    // the first 10% of the controller's duration.

    opacity = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.0,
          0.100,
          curve: Curves.ease,
        ),
      ),
    ),

    // ... Other tween definitions ...
    );

  final AnimationController controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius?> borderRadius;
  final Animation<Color?> color;

  // This function is called each time the controller "ticks" a new frame.
  // When it runs, all of the animation's values will have been
  // updated to reflect the controller's current value.
  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Container(
      padding: padding.value,
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: width.value,
          height: height.value,
          decoration: BoxDecoration(
            color: color.value,
            border: Border.all(
              color: Colors.indigo[300]!,
              width: 3,
            ),
            borderRadius: borderRadius.value,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

有状态组件:StaggerDemo

#

有状态组件 StaggerDemo 创建 AnimationController(所有动画的管理者),并指定 2000 毫秒的持续时间。它播放动画,并构建组件树的非动画部分。当在屏幕上检测到点击时,动画开始。动画先向前运行,然后向后运行。

dart
class StaggerDemo extends StatefulWidget {
  @override
  State<StaggerDemo> createState() => _StaggerDemoState();
}

class _StaggerDemoState extends State<StaggerDemo>
    with TickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
  }

  // ...Boilerplate...

  Future<void> _playAnimation() async {
    try {
      await _controller.forward().orCancel;
      await _controller.reverse().orCancel;
    } on TickerCanceled {
      // The animation got canceled, probably because it was disposed of.
    }
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 10.0; // 1.0 is normal animation speed.
    return Scaffold(
      appBar: AppBar(
        title: const Text('Staggered Animation'),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: Center(
          child: Container(
            width: 300,
            height: 300,
            decoration: BoxDecoration(
              color: Colors.black.withValues(alpha: 0.1),
              border: Border.all(
                color: Colors.black.withValues(alpha: 0.5),
              ),
            ),
            child: StaggerAnimation(controller:_controller.view),
          ),
        ),
      ),
    );
  }
}