交织动画

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

Staggered animations are a straightforward concept: visual changes happen as a series of operations, rather than all at once. The animation might be purely sequential, with one change occuring after the next, or it might partially or completely overlap. It might also have gaps, where no changes occur.

本指南展示如何在Flutter中构建交织动画。

This guide shows how to build a staggered animation in Flutter.

The following video demonstrates the animation performed by basic_staggered_animation:

在这个视频中,你可以看到一个独立的 widget 的以下动画,以一个带边框的略微有圆角的蓝色矩形开始,这个矩形会按照以下顺序变化:

In the video, you see the following animation of a single widget, which begins as a bordered blue square with slightly rounded corners. The square runs through changes in the following order:

  1. 淡出

    Fades in

  2. 扩大

    Widens

  3. 向上移动同时变得更高

    Becomes taller while moving upwards

  4. 变为一个有边框的圆圈

    Transforms into a bordered circle

  5. 颜色变为橙色

    Changes color to orange

向前运行之后,动画将反向运行。

After running forward, the animation runs in reverse.

一个交织动画的基础结构

Basic structure of a staggered animation

下图展示了在 basic_staggered_animation 使用间隔的例子。你会注意到有以下特点:

The following diagram shows the Intervals used in the basic_staggered_animation example. You might notice the following characteristics:

  • 透明度在时间轴的前 10% 发生变化。

    The opacity changes during the first 10% of the timeline.

  • 透明度的变化和宽度的变化之间有一个很小的间隔。

    A tiny gap occurs between the change in opacity, and the change in width.

  • 在时间轴的最后 25% 没有动画。

    Nothing animates during the last 25% of the timeline.

  • 增加填充使 widget 看起来向上上升。

    Increasing the padding makes the widget appear to rise upward.

  • 将圆角半径增加到 0.5,将圆角正方形变成一个圆。

    Increasing the border radius to 0.5, transforms the square with rounded corners into a circle.

  • 填充和高度的变化发生在相同的时间间隔内,但它们不必这么做。

    The padding and height changes occur during the same exact interval, but they don’t have to.

Diagram showing the interval specified for each motion

设置这个动画:

To set up the animation:

  • 创建一个 AnimationController 管理所有的 Animations

    Create an AnimationController that manages all of the Animations.

  • 为每一个有动画的属性创建一个 Tween

    Create a Tween for each property being animated.

    • Tween 定义一个值的范围。

      The Tween defines a range of values.

    • Tween 的 animate 方法需要 parent 控制器。同时生成一个动画为这个属性。

      The Tween’s animate method requires the parent controller, and produces an Animation for that property.

  • 指定动画的 “curve” 属性的间隔

    Specify the interval on the Animation’s curve property.

当控制动画的值发生变化时,新动画的值也随之变化值更改,触发 UI 更新。

When the controlling animation’s value changes, the new animation’s value changes, triggering the UI to update.

下面的代码为 width 属性创建了一个 tween。

The following code creates a tween for the width property.

它创建了一个 CurvedAnimation, 指定一个 eased curve。其他更多的预定的动画曲线请看 Curves

It builds a CurvedAnimation, specifying an eased curve. See Curves for other available pre-defined animation curves.

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

beginend 的值不一定是 doubles。

The begin and end values don’t have to be doubles.

下面的代码为 borderRadius 属性创建一个 tween(控制矩形的圆角半径),使用 BorderRadius.circular()

The following code builds the tween for the borderRadius property (which controls the roundness of the square’s corners), using BorderRadius.circular().

borderRadius = BorderRadiusTween(
  begin: BorderRadius.circular(4.0),
  end: BorderRadius.circular(75.0),
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.375, 0.500,
      curve: Curves.ease,
    ),
  ),
),

完整的交织动画

Complete staggered animation

像所有可交互的 widgets 一样,完整的动画包括一对 widget:一个无状态 widget 和一个有状态的 widget。

Like all interactive widgets, the complete animation consists of a widget pair: a stateless and a stateful widget.

无状态 widget 指定 Tweens,定义动画对象,提供一个 build() 方法,负责构建 widget 树的动画部分。

The stateless widget specifies the Tweens, defines the Animation objects, and provides a build() function responsible for building the animating portion of the widget tree.

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

The stateful widget creates the controller, plays the animation, and builds the non-animating portion of the widget tree. The animation begins when a tap is detected anywhere in the screen.

Full code for basic_staggered_animation’s main.dart

无状态的 widget: StaggerAnimation

Stateless widget: StaggerAnimation

在无状态 widget 中,StaggerAnimation,the build() 函数实例化了一个 AnimatedBuilder—一个用于构建动画的通用 widget。 AnimatedBuilder 构建一个 widget 并使用 Tweens 的当前值配置它。这个例子创建一个名为 _buildAnimation() (实际更新 UI)的方法,并将其分配给其 builder 属性。AnimatedBuilder 监听来自动画控制器的通知,当值发生更改时,将 widget 树标记为 dirty。对于动画的每一个标记,值都会更新,导致调用 _buildAnimation()

In the stateless widget, StaggerAnimation, the build() function instantiates an AnimatedBuilder—a general purpose widget for building animations. The AnimatedBuilder builds a widget and configures it using the Tweens’ current values. The example creates a function named _buildAnimation() (which performs the actual UI updates), and assigns it to its builder property. AnimatedBuilder listens to notifications from the animation controller, marking the widget tree dirty as values change. For each tick of the animation, the values are updated, resulting in a call to _buildAnimation().

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({ Key key, 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: Interval(
          0.0, 0.100,
          curve: Curves.ease,
        ),
      ),
    ),

    // ... Other tween definitions ...

    super(key: key);

  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.0,
            ),
            borderRadius: borderRadius.value,
          ),
        ),
      ),
    );
  }

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

有状态的 widget: StaggerDemo

Stateful widget: StaggerDemo

有状态的 widget, StaggerDemo,创建 AnimationController(控制所有动画的控制器),设定一个 2000 毫秒的周期。控制器播放一个动画,然后在 widget 树上创建一个无动画的部分。当在屏幕上检测到一个点击时,动画开始。动画向前运行,然后向后运行。

The stateful widget, StaggerDemo, creates the AnimationController (the one who rules them all), specifying a 2000 ms duration. It plays the animation, and builds the non-animating portion of the widget tree. The animation begins when a tap is detected in the screen. The animation runs forward, then backward.

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

class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
  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.0,
            height: 300.0,
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(0.1),
              border: Border.all(
                color:  Colors.black.withOpacity(0.5),
              ),
            ),
            child: StaggerAnimation(
              controller: _controller.view
            ),
          ),
        ),
      ),
    );
  }
}

资源

Resources

以下资源可能会在编写动画时有所帮助:

The following resources might help when writing animations:

[动画效果介绍][Animations landing page]
Flutter 动画效果文档的合集页面,如果你刚接触,可以从 这个教程开始。

[Animations landing page][]
Lists the available documentation for Flutter animations. If tweens are new to you, check out the Animations tutorial.

[Flutter API 文档][Flutter API documentation]
Flutter 库所有的参考文档。特别是 [animation library][] 文档。

[Flutter API documentation][]
Reference documentation for all of the Flutter libraries. In particular, see the [animation library][] documentation.

[Flutter Gallery][]
Demo 应用程序展示了许多 Material Design 组件和其他 Flutter 特性。[Shrine demo][] 执行了一个 hero 动画。

[Flutter Gallery][]
Demo app showcasing many Material Components and other Flutter features. The [Shrine demo][] implements a hero animation.

Material 动画效果指导文档
Material 动效果文档。

Material motion spec
Describes motion for Material apps.