跟着Flutter官方教程学动画

563 阅读13分钟

本文章翻译自👉Flutter 官方教程,看完本文章大家就会使用前几篇文章介绍的关键类,就会知道什么时候使用 AnimatedWidget,什么时候使用 AnimatedBuilder 了。

以下是文章正文


本教程向开发者介绍了,如何在 Flutter 中开发显式动画。会写介绍动画仓库中一些关键的概念、类和方法,使用这些关键的点开发 5 个动画示例。 这些动画相互构建穿插,比较全面的展示动画库。

Flutter SDK 也内置了一些显示动画,比如 FadeTransitionSizeTransitionSlideTransition 。这些简单的动画只需要设置开始和结束点就可以触发,比自定义的显示动画容易的多。自定义的

本教程向您展示了如何在Flutter中构建显式动画。在介绍动画库中的一些基本概念、类和方法之后,它会带你走过5个动画示例。这些示例相互构建,向您介绍动画库的不同方面。本篇文章这些动画都会涉及到。

关键概念和关键类

  • Animation,Flutter 动画库的核心类,提供动画的值
  • Animation 对象知道动画的状态,但是不知道动画在屏幕上显示的 UI
  • AnimationController 管理 Animation
  • CurvedAnimation 定义一个非线性的动画过程
  • Tween 提供动画的补间值或者效果值,比如补间可以定义为从红色到蓝色等等
  • 使用 ListenerStatusListener 来监听动画状态的变化

Widget 使用动画的方式一般有两种,一种是在 build 方法中直接使用动画的值和状态值,一种是将动画传递给其他的 Widget,其他的 Widget 使用传递的动画实现更复杂的效果。

Animation<double>

在 Flutter 中,Animation 对象不知道啥显示到了屏幕上,它仅仅是一个抽象类,只定义了当前的动画值和动画状态。最常用的动画类型是 Animation<double>

Animation 对象会时间范围内序列地生成插值,这个插值在定义的上下界之间,Animation 对象的生成的插值,可能是线性的,可能是曲线的,可能是跳跃的,也可以是开发者自定义的映射。只要我们控制了 Animation 对象,就可以实现反转,甚至可以在中间切换动画的方向。

动画也可以是指定的类型,比如颜色动画 Animation<Color> 或者 Animation<Size>

Animation 对象是有状态的,.value 成员变量可以获取动画当前的值,

Animation 对渲染和 build() 方法无感知。

CurvedAnimation

CurvedAnimation 定义了使用非线性的曲线定义了动画的过程。

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

注意  
Curves 定义了许多常用的动画曲线,开发者也可以像下面一样自定义:

import 'dart:math';

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

大家可以在 Curves文档中浏览 Flutter 内置的曲线效果。

CurvedAnimation 和 AnimationController 都是 Animation<double> 的子类, 所以它们可以相互传递。那么区别是啥呢? CurvedAnimation 就是修改了原来的动画值实现了曲线效果。但是 AnimationController 不需要泛化来实现曲线。

AnimationController

By default, an AnimationController linearly produces the numbers from 0.0 to 1.0 during a given duration. For example, this code creates an Animation object, but does not start it running:

AnimationController 是一个特殊的 Animation 对象,它把动画值的产生与帧关联起来了,只要产生新的帧就产生一个动画值。默认的情况下,AnimationController 会在动画的时间范围内,线性的在 0.0 到 1.0 之间产生值。比如,下面的代码会构造一个 Animation 对象:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

从类结构来说,AnimationController 是 Animation<double> 的泛化,因此,只要是需要 Animation 对象的地方,都可以传一个 AnimationController 对象。和Animation<double> 相比,AnimationController 提供了动画控制的方法,比如可以调用 .forward() 来开始一个动画。

上面提到了,AnimationController 将值的产生和屏幕刷新关联起来了,所以一般情况下,每秒生成60个数字。只要产生新的值,那么就会调用 Listener 监听者。如果想在帧产生的时候,创建自定义的展示列表可以参考一下 RepaintBoundary ,这个组件定义了绘制的边界。

创建 AnimationController 对象的时候,需要传一个 vsync 参数,vsync 的作用是熄屏时减少动画的损耗。在 StatefulWidget 的 State 中使用动画的话,可以混入 SingleTickerProviderStateMixin 。可以参考 👉animate1

Tween

一般来说,AnimationController 的变化范围是从 0.0 到 1.0。如果想要不同的变化范围,不同的数据类型,那么可以使用 TweenTween 定义了动画的插值范围或者插值类型。比如下面定义了 -200.0 到 0.0:

tween = Tween<double>(begin: -200, end: 0);

Tween 是无状态的,仅仅接受 begin 和 end,它完成输入到输出的映射。输入一般来说是 0.0 到 1.0,但是不固定。

Tween 继承自 Animatable<T>,不是继承自 Animation<T>Animatable 不必输出 double 类型的值。比如 ColorTween 指定了两个颜色之间的变化过程。

colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween 对象不会存储任何状态,它定义了 evaluate(Animation<double> animation) 方法,该方法呢把当前动画的值应用到映射函数上。 Animation 的 .value 可以取到当前动画的值,evaluate 方法还会执行一些常规的操作,比如 0.0 的时候返回 begin , 1.0 的时候返回 end。

Tween.animate

想要使用 Tween 过程的话,就需要调用 Tween 的 animate() 方法,这个方法需要一个 controller 对象。比如下面的代码的含义:在 500 ms 内,生成一系列 0 到 255 内的 int 值。

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

注意:  animate() 方法返回的是 Animation 对象,不是 Animatable 对象。

下面的代码结合了: controller, curve, tween:

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

动画通知

可以调用 addListener() 和 addStatusListener() 来监听值的变化和状态的变化。 addListener() 内通常会调用 setState() 来重新构建。addStatusListener() 通常是关心动画的开始、结束、移动、反转等状态的变化。

动画例子

下面是五个动画的例子

渲染动画

  • 如何使用addListener() 和 setState() 来给一个普通的Widget加上动画
  • 每产生一个动画值,都在addListener() 方法中调用 setState()
  • 如何定义一个带有 vsync 必须参数的 AnimationController
  • 通过 “..addListener” 理解 “..” 关键字,
  • 使用 _ 开头来让类私有

目前我们已经知道如何产生动画需要的值,但是呢还没有将值与显示绑定出来。想要使用 Animation 对象渲染的话,需要把 Animation 对象作为 我们 Widget 的成员变量,然后决定怎么使用动画的值进行绘制。

下面是不带动画效果的 Flutter 的 Logo

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

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

  @override
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: const FlutterLogo(),
      ),
    );
  }
}

Logo 动画的从无到满屏,需要以下的步骤:

第一步:定义 AnimationController,传入动画持续时间和 vsyncvsync 的构造需要借助 SingleTickerProviderStateMixin

第二步:使用 Tween<double> 将 0-1 转为 0-300

第三步:添加动画的监听者,addListener 方法体中调用 setState,持续的重续构建

第四步:把原来指定的宽高变为动画的宽高

第五步:在 dispose中释放controller,避免内存泄漏

代码如下:


class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    //第一步
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // #docregion addListener
    //第二步
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        // #enddocregion addListener
        //第三步
        setState(() {
          // The state that has changed here is the animation object’s value.
        });
        // #docregion addListener
      });
    // #enddocregion addListener
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        //第四步
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
    //第五步
    controller.dispose();
    super.dispose();
  }
}

效果如下:

size变化 (1).gif

代码中声明动画的地方用到了 ..,表示返回值的基础上调用方法。

animation = Tween<double>(begin: 0, end: 300).animate(controller)
  ..addListener(() {
    // ···
  });
  
animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation.addListener(() {
    // ···
  });
这两段代码含义是相同的,都是声明动画对象,并且添加监听者

使用 AnimatedWidget 简化

  • 如何使用 AnimatedWidget 代替addListener + setState 的方式来创建动画
  • 使用 AnimatedWidget 创建一个可复用动画的 Widget。AnimatedBuilder 可以实现 Widget 与 动画分离。

The AnimatedWidget base class allows you to separate out the core widget code from the animation code. AnimatedWidget doesn’t need to maintain a State object to hold the animation. Add the following AnimatedLogo class:

AnimatedWidget 基类可以将 核心Widget 从动画代码中分离出来。AnimatedWidget不再需要维护添加动画的 State对象。如下所示:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

// #docregion AnimatedLogo
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}
// #enddocregion AnimatedLogo

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

  @override
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

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

和第一个代码相比, AnimatedLogo是专门创建执行动画的 Widget,LogoApp 是生成动画对象的地方。这样两者就分离了。实现的效果是一样的。

监听动画过程

一般来说,开发者经常需要知道动画状态的改变,比如完成、向前、反转等等。我们可以通过 addStatusListener() 来得到通知。我们可以在前面案例基础上进行修改,来监听和打印动画的状态值。

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      //第一处
      ..addStatusListener((state) => print('$state'));
    controller.forward();
  }
  // ...
}

看第一处的代码,我们增加了状态的监听,并且打印了状态。 控制台会如下打印:

AnimationStatus.forward
AnimationStatus.completed

我们可以通过状态的监听来实现无限循环的效果。在到 1 的时候,反向动画。在到 0 的时候正向动画。

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
        ..addStatusListener((status) {
            //第一处
            if (status == AnimationStatus.completed) {
              controller.reverse();
            } else if (status == AnimationStatus.dismissed) {
              controller.forward();
            }
      })
    controller.forward();
  }
  // ...
}

使用 AnimatedBuilder 重构

我们前一个LogoApp 和 AnimatedLogo 配合的代码有一点问题,我们每次动画会调用_LogoAppState 的 build 方法,然后就会构建新的 AnimatedLogo,AnimatedLogo 就会构建,然后每次都会构建新的 FlutterLogo,其实 FlutterLogo 是不用构建的,只要构建它上层的 Container,修改大小即可。所以问题就是 每次多构建了 FlutterLogo。 一个解决此问题的办法是:把功能分离到不同的类:

  • 渲染 logo
  • 定义 Animation 对象
  • 渲染动画

可以使用 AnimatedBuilder 来实现分离的效果。在渲染树上,AnimatedBuilder 是一个单独的类。和 AnimatedWidget 一样, AnimatedBuilder 会自动监听 Animation 对象发出的通知,比如值的变化、状态的变化,并且在动画值变化的时候自动将 Widget 树标记为的,所以开发者不需要自己调用 addListener() 方法。

代码如下:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
  const LogoWidget({Key? key}) : super(key: key);

  // Leave out the height and width so it fills the animating parent
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
  }
}
// #enddocregion LogoWidget

// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
  const GrowTransition({required this.child, required this.animation, Key? key})
      : super(key: key);

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}
// #enddocregion GrowTransition

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

  @override
  _LogoAppState createState() => _LogoAppState();
}

// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }
  // #enddocregion print-state

  @override
  Widget build(BuildContext context) {
    return GrowTransition(
      child: const LogoWidget(),
      animation: animation,
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
// #docregion print-state
}

上面代码形成的 Widget 树是这样的:

AnimatedBuilder-WidgetTree.png

从 Widget 树的底部开始看,LogoWidget 是渲染 logo 的组件,很简单:

class LogoWidget extends StatelessWidget {
  const LogoWidget({Key? key}) : super(key: key);

  // Leave out the height and width so it fills the animating parent
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
  }
}

LogoWidget 移除了宽高的设置,就是填充满父布局,至于父布局是多少它不关心。

中间的三块都是在 GrowTransition 的 build() 方法中构建的,下面的代码会展示出来。 GrowTransition 本身是无状态的并且持有 final 的定义动画的 animation 属性。 build() 方法构造了 AnimatedBuilder,这个 AnimatedBuilder 把匿名方法和 LogoWidget 对象作为入参。渲染和动画就在匿名方法中,我们看到匿名方法的 SizedBox 的宽高是动画的值,就是强制 child(LogoWidget)是动画的宽高,达到了动画缩放的效果。

class GrowTransition extends StatelessWidget {
  const GrowTransition({required this.child, required this.animation, Key? key})
      : super(key: key);

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

把这些组合起来的代码,有点像最开始的动画代码。在 initState() 中创建 AnimationController 和 Tween,使用 animate() 把他们关联起来。 是不是很神奇动画就这样实现了,build() 方法返回了 GrowTransitionGrowTransition 需要一个字节点 LogoWidget ,和一个动画对象,这就是上面列出来的。

同时动画

下面,我们在前面讲到的利用动画状态实现循环的基础上,实现同时动画的效果。AnimatedWidget 实现了从无到有的动画过程。想象一下,我们在从无到有的基础上,再加上透明度的变化。

例子主要展示了在一个 controller 上使用多个 tween,每一个 tween 实现一种动画效果。

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);

sizeAnimation.value 可以得到尺寸动画的值,opacityAnimation.value 可以得到透明度动画的值。但是呢 AnimatedWidget 仅仅需要一个 Animation 对象。为了解决这个问题,例子中给的方式是 AnimatedLogo 创建自己的 Tween 对象,并且显示的计算值。

具体的代码是这样的,AnimatedLogo 封装了自己的 Tween 对象,并且在 build 方法中调用 Tween.evaluate() 犯法,evaluate 计算的依据是父动画的值。计算的结果是尺寸和透明度的值。

具体的代码是这样的:

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

  // Make the Tweens static because they don't change.
  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween = Tween<double>(begin: 0, end: 300);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: const EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: const FlutterLogo(),
        ),
      ),
    );
  }
}

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

  @override
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

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

前面的代码没有变,变的只是 AnimatedLogo 。整体效果如下:

同时 (1).gif

总结

本节主要是讲了一些 Flutter 动画的基础,比如 TweensAnimaitonController 等等。还有很多类开发者可以继续探索。大家如果想看复杂的 Tweens、Material 设计的动画、Hero 动画等可以看之前的 一文可以让你在Flutter动画上讲两句里面有很多资源。