Flutter之动画教程

498 阅读12分钟
你会学到什么
·如何使用动画库中的基本类将动画添加到小部件。
·什么时候使用`AnimatedWidget`vs`AnimatedBuilder`

本教程向您展示如何在Flutter中构建显示动画。在介绍了动画库中的一些基本概念、类和方法之后,它带您完成了5个动画示例。这些示例相互依存,向您介绍动画库的不同方面。

Flutter SDK还提供了内置的显式动画,例如FadeTransitionSizeTransitionSlideTransition。这些简单地动画是通过是设置起点和终点来触发的。它们比此处描述的自定义显式动画更易于实现。

一、基本动画概念和类

重点是什么?
· `Animation`是Flutter动画库中的核心类,它插入用于引导动画的值。
· `Animation`对象知道动画的当前状态(例如,它是开始、停止还是向前或向后移动),但对屏幕上显示的内容一无所知。
· `AnimationController`管理动画。
· `CurvedAnimation`将进程定义为非线性曲线。
· `Tween`在动画对象使用的数据范围之间进行插值。例如,补间可以定义从红色到蓝色或从0255的插值。
· 使用`Listeners``StatusListeners`来监听动画状态的变化。

Flutter中的动画系统是基于类型化的Animation对象。小部件可以通过读取它们的当前值并监听它们的状态变化,直接将这些动画合并到他们的构建函数中,或者它们可以使用这些动画作为它们传递给其他小部件的更精细动画的基础。

1.1、Animation<double>

在Flutter中,Animation对象对屏幕上的内容一无所知。Animation是一个抽象类,它了解其当前值及其状态(已完成或已解除)。一种更常用的动画类型Animation<double>

Animation对象在一定持续时间内按顺序生成两个值之间的插值。Animation对象的是撒输出可能试试线性、曲线、阶跃函数或您可以设计的任何其他映射。根据Animation对象的控制方式,它可以反向运行,甚至可以在中间切换方向。

Animations还可以插入除double之外的类型,例如,Animation<Color>Animation<Size>

Animation对象具有状态。它的当前值始终在.value成员中可用。

Animation对象对渲染或build()函数一无所知。

1.2、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);
}

浏览文档以获取 Flutter 附带的常量Curves的完整列表(带有可视化预览)

CurvedAnimationAnimationController(在下一节中介绍)都是Animation<double>类型,因此您可以交替传递它们。CurvedAnimation包装了它正在修改的对象——您不需要继承AnimationController来实现曲线。

1.3、AnimationController

AnimationControllre是一个特殊的Animation对象,只要硬件准备好接收新帧,它就会生成一个新值。默认情况下,AnimationController在给定的持续时间内线性生成从0.0到1.0的数字。例如,这段代码创建了一个Animation对象,但没有启动它运行:

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

AnimationController派生自Animation<double>,因此可以在需要Animation对象的任何地方使用它。然而,AnimationController有额外的方法来控制动画。例如,您使用.forward()方法启动动画。数字的生成与屏幕刷新有关,因此通常每秒生成60个数值。每个数字生成后,每个Animation对象都会调用附加的Listener对象。要为每个子项创建自定义显示列表,请参阅RepaintBoundary.

创建AnimationController时,您向它传递一个vsync参数。vsync的存在可以防止屏幕外动画消耗不必要的资源。您可以通过将SingleTickerProviderStateMixin添加到类定义来将状态对象用作vsync。您可以在Github上的animate1中看到这方面的示例。

注意:在某些情况下,位置可能会超过`AnimationController`0.0-1.0范围。例如,`fling()`函数允许您提供双速度、力和位置(通过Force对象)。位置可以是任何东西,因此可以在0.01.0范围之外。

`CurvedAnimation`也可以超过0.01.0的范围,即使`AnimationController`没有。根据所选曲线,`CurvedAnimation`的输出范围可以比输入范围更宽。例如`Curves.elasticIn`等弹性曲线明显超出或低于默认范围。

1.4、Tween

默认情况下,AnimationController对象的范围从0.0到1.0。如果您需要不同的范围换或不同的数据类型,您可以使用Tween配置动画以插入到不同的范围或数据类型 。例如,一下Tween-200.0变为0.0:

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

Tween是一个无状态对象,只有开始和结束。Tween的唯一工作是定义从输入范围到输出范围的映射。输入范围通常为0.01.0,但这不是必须的。

Tween继承自Animatable<T>,而不是Animation<T>AnimatableAnimation一样,不必输出双倍。例如,ColorTween指定两种颜色之间的渐变。

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

Tween对象不存储任何状态。相反,它提供了evaluate(Animation<double> animation)方法,该方法使用转换函数将动画的当前值(介于0.0和1.0之间)映射到实际动画值。

Animation对象的当前值可以在.value方法中找到。evaluate函数还执行一些内务处理,例如确保在动画值分别为0.01.0时返回begin和end。

1.4.1、Tween.animate

要使用Tween对象,请在Tween上调用animate()并传入控制器对象。例如,以下代码在500毫秒的过程中生成从0到255的整数值。

AnimationController controller = AnimationController(duration: const Duration(millisecond: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
注意:`animate()`方法返回一个`Animation`,而不是一个`Animatable`

以下示例显示了一个控制器、一条曲线和一个Tween:

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

1.5、Animation notifications

Animation对象可以有ListenersStatusListeners,用addListener()addStatusListener()定义。只要动画的值发生变化,就会调用ListenerListener最常见的行为是调用setState()以引起重建。当动画开始、结束、向前移动或向后移动时调用StatusListener,如AnimationStatus所定义。

二、动画示例

本节将向您介绍5个动画示例。每个部分都提供了指向该示例源代码的链接。

2.1、渲染动画

重点是什么?
· 如何使用`addListener()``setState()`将基本动画添加到小部件。
· 每次`Animaaation`生成一个新数字时,`addLiistener()`函数都会调用`setStaate()`。
· 如何撒使用所需的`vsync`参数定义`AnimationController`。
· 了解`..addListener`中的`..`语法,也称为Dart的级联符号。
· 要将类设置为私有,其名称应以下划线(_)开头。

到目前为止,您已经了解了如何随时间生成数字序列。没有任何东西被渲染到屏幕身上。要使用Animation对象进行渲染,请将Animation对象存储为小部件的成员,然后随时用其值来决定如何绘制。

考虑以下绘制没有动画的Flutter Logo的应用程序:

import 'package:flutter/material.dart';

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

class LogoApp extends StatefulWidget {
    const LogoApp({Key? key): super(key: key);
    
    @override
    State<LogoApp> createState() => _LogoAppState();
}

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

应用来源:  animate0

下面显示了修改后的相同代码,使Logo具有动画效果,从无到有增长的完整大小。在定义AAnimationController时,必须传入一个vsync对象。vsync参数在AnimationController部分中进行了描述。

突出显示了非动画示例的修改:

image.png

addListener()函数调用setState(),因此每次Animatioon生成新数字时,当前帧 都被标记为dirty,这会强制再次调用build()。在build()中,容器改变了大小,因为它的高度和宽度现在使用animation.value而不是硬编码值。在丢弃State对象时处理控制器,以防止内存泄露。

通过这些是少量更改,您已经在Flutter中创建了您的第一个动画!

Dart语言技巧:您可能不熟悉Dart的级联表示法——..addListener()中的两个点。此语法意味着使用animate()的返回值调用addListener()方法。 以下是示例:

image.png

## 2.2、使用`AnimatedWidget`进行简化 重点是什么? · 如何使用`AnimatedWidget`帮助程序类(而不是`addListener()`和`setState()`)来创建动画小部件。 · 使用`AnimatedWidget`创建一个执行重用动画的小部件。要将过渡的小部件分开,请使用`AnimatedBuilder`,如使用`AniimatedBuilder`重构部分所示。 · Flutter API中的`AnimatedWidget`示例:`AnimatedBuilder`、`AnimatedModalBarrier`、`DecoratedBoxTransiition`、`FadeTransition`、`PositionedTransition`、`RelativePositionedTransition`、`RotationTransition`、`ScaleTransition`、`SizeTransition`、`SlideTransition`。

AnimatedWidget基类允许您将核心小部件代码从动画代码中分离出来。AnimatedWidget不需要维护一个State对象来保存动画。添加以下AnimatedLogo类:

class AnimatedLogo extends AnimatedWidget {
    const AnimatedLogo({super.key, required Animation<double> animation}}: super(listenable: animation);
    
    @override
    Widget build(BuildContext context) {
        final animation = listenable as Animation<double>;
        return Center(
            child: Container(
                margin: const EdgeIInsets.symmetric(vertical: 10),
                height: animation.value,
                width: animation.value,
                child: const FlutterLogo(),
            ),
        );
    }
}

AnimationedLogo在绘制自身时使用Animation的当前值。

LogoApp仍然管理AnimationControllerTween,并将Animation对象传递给AnimatedLogo

image.png

2.3、控制动画进度

重点是什么?
· 使用`addStatusListener()`通知动画状态的变化,例如开始、停止或反转方向。
· 当动画完成或返回到其开始状态时,通过反转方向在无线循环中运行动画。

了解动画何时更改状态通常很有帮助,例如完成、前进或倒退。您可以使用addStatusListener()通知。以下代码修改了前面的示例,以便它侦听状态更改并打印更新。突出显示的行显示更改:

image.png

运行此代码会产生以下输出: ``` AnimationStatus.forward AnimationStatus.completed ``` 接下来,使用`addStatusListener()`在开始或结束时反转动画。这会善生“呼吸”效果:

image.png

2.4、使用AnimatedBuilder进行重构

重点是什么?
· `AnimatedBuilder`了解如何呈现过渡。
· `AnimatedBuilder`不知道如何渲染小部件,也不知道如何管理`Animation`对象。
· 使用`AnimatedBuilder`将动画描述为另一个小部件的构建方法的一部分。如果您只是想定义一个带有可重用动画的小部件,请使用`AnimatedWidget`,如使用`AnimatedWidget`进行简化部分所示。
· Flutter API中的`AnimatedBuilder`示例:`BottomSheet``ExpansionTile``PopupMenu``ProgressIndicator``ReefreshIndicator``Scaffold``SnackBar``TabBar``TextField`

animate3示例中的代码存在一个问题,即更改动画需要更改呈现Logo的小部件。更好地解决方案实施将职责分离到不同的类中:

  • 渲染Logo
  • 定义Animation对象
  • 渲染过渡

您可以在AnimatedBuilder类的帮助下完成这种分离。AnimatedBuilder是渲染树中的一个单独类。与AnimatedWidget一样,AnimatedWidget会自动侦听来自Animation对象的通知,并在必要时将小部件树标记为dirty,因此您无需调用addListener()

animate4示例的小部件树如下所示:

image.png

从小部件树的底部开始,渲染Logo的代码很简单:
class LogoWidget extends StatelessWidget {
    const LogoWidget(Key? key): super(keu: 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(),
        );
    }
}

图中间的三块都是在GrowTransition中的build()方法中创建的,如下所示。GrowTransition小部件本身是无状态的,并包含定义过渡动画所需的一组最终变量。build()函数创建并返回AnimatedBuilder,它以(Anonymous builder)方法和LogoWidget对象作为参数。渲染过渡的工作实际上发生在(Annoymous builder)方法中,该方法创建一个适当大小的Container以强制LogoWidget收缩以适应。

下面代码中的一个棘手点时候子项看起来像是被指定了李爱你告辞。发生的事情是child的外部引用被传递给AnimatedBuilderAnimatedBuilder将其传递给匿名闭包,然后使用该对象作为其子对象。最终结果是AnimatedBuilder被插入到渲染树中的两个小部件之间。

class GrowTransition extends StatelessWidget {
    const GrowTransition({required this.child, required this.animation});
    
    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,
            ),
        );
    }
}

最后,初始化动画的代码看起来与animate2示例非常相似。initState()方法创建一个AnimationControllerTween,然后将它们与animate()绑定。魔法发生在build()方法中,该方法返回一个带有LogoWidget作为子对象的GrowTransition对象,以及一个驱动转换的动画对象。这些是上面要点中列出的三个要素。

image.png

image.png

2.5、同步动画

重点是什么?
· `Curves`类定义可与`CurvedAnimation`一起随时用的常用曲线数组

在本节中,您将以监控动画进度,该示例使用AnimatedWidget来连续动画进出。考虑这样一种情况,当不透明度从透明度变为不透明时,您想要动画进出。

注意:此示例展示了如何在同一个动画控制器上使用多个补间,其中每个补间管理动画中的不同效果。它仅用于说明目的。如果您在生产代码中补间不透明度和大小,您可能会改用`FadeTransition``SizeTransition`

每个补间管理动画的一个方面。例如:

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对象。为了解决这个问题,该示例创建了自己的Tween对象并显示计算值。

改变AnimatedLogo来封装它自己的Tween对象,它的build()方法调用父动画对象上的Tween.evaluate()来计算所需的大小和不透明度值。以下代码突出显示了更改:

image.png

image.png