Flutter 最后一个动画交错动画

3,671 阅读5分钟

本文翻译自 👉Staggered animations,是官方给出的最后一个动画教程——交错动画。看完本篇让你的动画不再单一,可以在淡入的过程中让尺寸变大,可以在尺寸变化的同时让形状变化。

本篇的内容

  • 交错动画可以是一个前后衔接的动画序列,也可以是重叠在一起的动画效果

  • 交错动画的创建需要使用多个 Animation 对象,这些动画对象的控制只有一个

  • 每一个动画对象都需要指定一个 Interval,来表明该动画在整个动画序列的起止时刻

  • 使用 Tween 来创建每一个动画

交错动画是一个很直接的概念:页面上的动画效果是一系列的动画,而不是一个动画一个动画手动加上去的。动画可能是纯粹的系列动画,动画之间前后衔接。也可能动画之间部分或者完全的重叠。也可能是空白动画,啥也没发生。

本篇就介绍如何在 Flutter 中构建交错动画。

案例

本篇文章使用基本的交错案例,更复杂的案例可以参考staggered_pic_selection。

我们完成的效果是这样的:

Stagger Demo (1).gif

交错动画的基本结构

  • 所有的动画都是被同一个 AnimationController 驱动的
  • 不管动画持续的时间,controller 的值必须在0-1的闭区间。
  • 每一个动画都有一个 0-1 闭区间 的Interval 的值
  • 单个的动画叫做 interval ,每一个 interval 创建一个 TweenTween 指定了开始
  • Tween 会创建动画并且动画由 controller 管理

下图演示了代码示例中 Interval 的使用,需要注意到以下的几点:

  • 透明度的改变占据了整个动画时长的10%

  • 透明度动画和宽度动画之间有一个小小的空档期

  • 在时间线的最后25%的时间内没有任何动画

  • Padding动画和高度变化动画是完全重叠在一起的,这两者动画是一起发生的

  • 增加了圆角到是为了让矩形变为圆形

  • 间距和高度动画完全重叠,是因为它们的 interval 一样,只要不一样就会错开

image.png

组装动画:

  • 创建一个 AnimationController 管理所有的动画

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

    • Tween 定义动画效果的范围 比如 透明度从0-1,颜色从绿色到白色

    • Tweenanimate 方法需要一个父动画,父动画就是 AnimationController

  • 指定动画的 curve 属性

只要动画的值发生变化,那么 UI 就会变化,这就是动画。

下面的代码创建了一个 width 属性的 Tween,并且指定了曲线。

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

beginend 属性不一定非得是 double 类型的值,下面的代码就是 borderRadius 值。

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

完成交错动画

就像所有交互式组件一样,完整的动画由一对 Widget 组成:一个 StatelessWidget 和一个 StatefulWidget

StatelessWidget 内指定 Tween,定义 Animation 对象,并在 build() 方法中构建待动画的 Widget 树。

StatefulWidget 创建 controller,驱动动画,构建非动画的 Widget 树。

Full code for basic_staggered_animation’s main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({Key? 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,
            ),
          ),
        ),
        width = Tween<double>(
          begin: 50.0,
          end: 150.0,
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.125,
              0.250,
              curve: Curves.ease,
            ),
          ),
        ),
        height = Tween<double>(begin: 50.0, end: 150.0).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.250,
              0.375,
              curve: Curves.ease,
            ),
          ),
        ),
        padding = EdgeInsetsTween(
          begin: const EdgeInsets.only(bottom: 16.0),
          end: const EdgeInsets.only(bottom: 75.0),
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.250,
              0.375,
              curve: Curves.ease,
            ),
          ),
        ),
        borderRadius = BorderRadiusTween(
          begin: BorderRadius.circular(4.0),
          end: BorderRadius.circular(75.0),
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.375,
              0.500,
              curve: Curves.ease,
            ),
          ),
        ),
        color = ColorTween(
          begin: Colors.indigo[100],
          end: Colors.orange[400],
        ).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.500,
              0.750,
              curve: Curves.ease,
            ),
          ),
        ),
        super(key: key);

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

class StaggerDemo extends StatefulWidget {
  const StaggerDemo({Key? key}) : super(key: key);

  @override
  _StaggerDemoState 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);
  }

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

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

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

void main() {
  runApp(
    const MaterialApp(
      home: StaggerDemo(),
    ),
  );
}

StatelessWidget: StaggerAnimation

在上面的 StaggerAnimation 类中,build() 方法构造了 AnimatedBuilder,目的是构造动画。AnimatedBuilder 使用 Tweens 的值构造待动画的 Widget 树。这个例子中调用了_buildAnimation() 方法, 并且把结果作为 builder 属性。AnimatedBuilder 会监听来自 controller 的通知, 标记 Widget 树需要重新构建。对于每一个动画值变化的 tick ,都会调用 _buildAnimation()

Statefulwidget: StaggerDemo

在上面的 StaggerDemo, 创造了 AnimationController ,这个 controller 管理所有的动画, 指定动画的时长是 2000 ms。 controller 驱动了动画,并且构建了不需要动画效果的 Widget 树。点击屏幕的时候会触发动画,并且动画完成的时候执行一个反向。

总结

至此,官方给出的动画文档全部翻译完成了,有概念介绍,比如什么是交错动画,什么是 Hero 动画。有类文档,比如 AnimatedBuilderAnimationController 等等。后面会把这些统一串起来。

一文可以让你在Flutter动画上讲两句

看完之后,你就能告诉别人哪个类实现了Flutter动画

跟着Flutter官方教程学动画

不知不觉到了 Hero 动画