交织动画

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

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

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

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

  1. 淡出

  2. 扩大

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

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

  5. 颜色变为橙色

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

一个交织动画的基础结构

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

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

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

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

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

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

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

Diagram showing the interval specified for each motion

设置这个动画:

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

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

    • Tween 定义一个值的范围。

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

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

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

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

它创建了一个 CurvedAnimation, 指定一个 eased curve。其他更多的预定的动画曲线请看 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。

下面的代码为 borderRadius 属性创建一个 tween(控制矩形的圆角半径),使用 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,
    ),
  ),
),

完整的交织动画

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

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

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

Full code for basic_staggered_animation’s main.dart

无状态的 widget: StaggerAnimation

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

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

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
            ),
          ),
        ),
      ),
    );
  }
}