隐式动画

欢迎来到隐式动画的 codelab,在这里您将学到:如何使用 Flutter widgets 轻松地对一组特定属性创建动画。

Welcome to the implicit animations codelab, where you learn how to use Flutter widgets that make it easy to create animations for a specific set of properties.

为了充分理解该 codelab,您应该具备以下基本知识:

To get the most out of this codelab, you should have basic knowledge about:

该 codelab 包括以下内容:

This codelab covers the following material:

  • 使用 AnimatedOpacity 来创建一个淡入效果。

    Using AnimatedOpacity to create a fade-in effect.

  • 使用 AnimatedContainer 让尺寸、颜色和边距产生动画变换。

    Using AnimatedContainer to animate transitions in size, color, and margin.

  • 隐式动画及其使用方法的概述。

    Overview of implicit animations and techniques for using them.

完成该 codelab 的时间约为:15-30 分钟。

Estimated time to complete this codelab: 15-30 minutes.

什么是隐式动画?

What are implicit animations?

通过使用 Flutter 的 动画库,您可以为 UI 中的组件添加运动和创建视觉效果。

With Flutter’s animation library, you can add motion and create visual effects for the widgets in your UI.

您可以使用库中的一套组件来管理动画,这些组件统称为隐式动画隐式动画组件,其名称源于它们都实现了 ImplicitlyAnimatedWidget 类。

One widget set in the library manages animations for you. These widgets are collectively referred to as implicit animations, or implicitly animated widgets, deriving their name from the ImplicitlyAnimatedWidget class that they implement.

使用隐式动画,您可以通过设置一个目标值,驱动 widget 的属性进行动画变换;每当目标值发生变化时,属性会从旧值逐渐更新到新值。通过这种方式,隐式动画内部实现了动画控制,从而能够方便地使用— 隐式动画组件会管理动画效果,用户不需要再进行额外的处理。

With implicit animations, you can animate a widget property by setting a target value; whenever that target value changes, the widget animates the property from the old value to the new one. In this way, implicit animations trade control for convenience—they manage animation effects so that you don’t have to.

示例:淡入文字效果

Example: Fade-in text effect

下面的示例展示了如何使用名为 AnimatedOpacity 的隐式动画 widget,为已存在的 UI 添加淡入效果。

The following example shows how to add a fade-in effect to existing UI using an implicitly animated widget called AnimatedOpacity.

这个示例开始没有动画效果— 它包含一个由 Material App 组成的主页面,有以下内容:

The example begins with no animation code—it consists of a Material App home screen containing:

  • 一张猫头鹰的照片。

    A photograph of an owl.

  • 一个点击时什么也不做的 Show details 按钮。

    One Show details button that does nothing when clicked.

  • 照片中猫头鹰的描述文字。

    Description text of the owl in the photograph.

淡入 (初始代码)

Fade-in (starter code)

点击 Run 按钮来运行这个示例:

Click the Run button to run the example:

{$ begin main.dart $}
import 'package:flutter/material.dart';

const owl_url = 'https://raw.githubusercontent.com/flutter/website/master/src/images/owl.jpg';

class FadeInDemo extends StatefulWidget {
  _FadeInDemoState createState() => _FadeInDemoState();
}

class _FadeInDemoState extends State<FadeInDemo> {
  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Image.network(owl_url),
      TextButton(
          child: Text(
            'Show details',
            style: TextStyle(color: Colors.blueAccent),
          ),
          onPressed: () => null),
      Container(
        child: Column(
          children: <Widget>[
            Text('Type: Owl'),
            Text('Age: 39'),
            Text('Employment: None'),
          ],
        ),
      )
    ]);
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: FadeInDemo(),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    MyApp(),
  );
}
{$ end main.dart $}

使用 AnimatedOpacity widget 进行透明度动画

Animate opacity with AnimatedOpacity widget

这部分包含在 淡入初始代码 中添加一个隐式动画一系列步骤。完成这些步骤后,您还可以运行 淡入完成代码,该代码已经实现了淡入效果。这些步骤概述了如何使用 AnimatedOpacity widget 来添加以下的动画特性:

This section contains a list of steps you can use to add an implicit animation to the fade-in starter code. After the steps, you can also run the fade-in complete code with the the changes already made. The steps outline how to use the AnimatedOpacity widget to add the following animation feature:

  • 用户点击 Show details 按钮后,显示猫头鹰的描述文字。

    The owl’s description text remains hidden until the user clicks the Show details button.

  • 当用户点击 Show details 按钮时,猫头鹰的描述文字淡入。

    When the user clicks the Show details button, the owl’s description text fades in.

1. 选择要进行动画的 widget 属性

1. Pick a widget property to animate

想要创建淡入效果,您可以使用 AnimatedOpacity widget 对 opacity 属性进行动画。将 Container widget 换成 AnimatedOpacity widget:

To create a fade-in effect, you can animate the opacity property using the AnimatedOpacity widget. Change the Container widget to an AnimatedOpacity widget:

{opacity1 → opacity2}/lib/main.dart
@@ -21,7 +21,7 @@
21
21
  style: TextStyle(color: Colors.blueAccent),
22
22
  ),
23
23
  onPressed: () => null),
24
- Container(
24
+ AnimatedOpacity(
25
25
  child: Column(
26
26
  children: <Widget>[
27
27
  Text('Type: Owl'),

2. 为动画属性初始化一个状态变量

2. Initialize a state variable for the animated property

opacity 的初始值设置为 0 ,以便在用户点击 Show details 前隐藏文字:

To hide the text before the user clicks Show details, set the starting value for opacity to zero:

{opacity2 → opacity3}/lib/main.dart
@@ -11,6 +11,8 @@
11
11
  }
12
12
  class _FadeInDemoState extends State<FadeInDemo> {
13
+ double opacity = 0.0;
14
+
13
15
  @override
14
16
  Widget build(BuildContext context) {
15
17
  return Column(children: <Widget>[
@@ -22,6 +24,8 @@
22
24
  ),
23
25
  onPressed: () => null),
24
26
  AnimatedOpacity(
27
+ duration: Duration(seconds: 3),
28
+ opacity: opacity,
25
29
  child: Column(
26
30
  children: <Widget>[
27
31
  Text('Type: Owl'),

3. 为动画设置一个触发器,并选择一个结束值

3. Set up a trigger for the animation, and choose an end value

当用户点击 Show details 按钮时,将会触发动画。为了做到这点,我们使用 TextButtononPressed() 方法,在调用时改变 opacity 的状态值为 1。

Configure the animation to trigger when the user clicks the Show details button. To do this, change opacity state using the onPressed() handler for TextlButton. To make the FadeInDemo widget become fully visible when the user clicks the Show details button, use the onPressed() handler to set opacity to 1:

{opacity4 → opacity5}/lib/main.dart
@@ -18,11 +18,14 @@
18
18
  return Column(children: <Widget>[
19
19
  Image.network(owl_url),
20
20
  TextButton(
21
- child: Text(
22
- 'Show Details',
23
- style: TextStyle(color: Colors.blueAccent),
24
- ),
25
- onPressed: () => null),
21
+ child: Text(
22
+ 'Show Details',
23
+ style: TextStyle(color: Colors.blueAccent),
24
+ ),
25
+ onPressed: () => setState(() {
26
+ opacity = 1;
27
+ }),
28
+ ),
26
29
  AnimatedOpacity(
27
30
  duration: Duration(seconds: 2),
28
31
  opacity: opacity,

4. 为动画设置时长

4. Set the duration of the animation

除了 opacity 参数,AnimatedOpacity 还需要 duration 参数确定动画时长。在下面的示例中,您可以设置淡入时长为 2 秒:

In addition to an opacity parameter, AnimatedOpacity requires a duration to use for its animation. For this example, you can start with 2 seconds:

{opacity3 → opacity4}/lib/main.dart
@@ -24,7 +24,7 @@
24
24
  ),
25
25
  onPressed: () => null),
26
26
  AnimatedOpacity(
27
- duration: Duration(seconds: 3),
27
+ duration: Duration(seconds: 2),
28
28
  opacity: opacity,
29
29
  child: Column(
30
30
  children: <Widget>[

淡入 (完成代码)

Fade-in (complete)

下面的示例是修改后的完成版代码— 运行这个示例,然后点击 Show details 按钮就可以触发动画。

Here’s the example with the completed changes you’ve made—run this example and click the Show details button to trigger the animation.

{$ begin main.dart $}
import 'package:flutter/material.dart';

const owl_url = 'https://raw.githubusercontent.com/flutter/website/master/src/images/owl.jpg';

class FadeInDemo extends StatefulWidget {
  _FadeInDemoState createState() => _FadeInDemoState();
}

class _FadeInDemoState extends State<FadeInDemo> {
  double opacityLevel = 0.0;

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Image.network(owl_url),
      TextButton(
        child: Text(
          'Show details',
          style: TextStyle(color: Colors.blueAccent),
        ),
        onPressed: () => setState(() {
          opacityLevel = 1.0;
        }),
      ),
      AnimatedOpacity(
        duration: Duration(seconds: 3),
        opacity: opacityLevel,
        child: Column(
          children: <Widget>[
            Text('Type: Owl'),
            Text('Age: 39'),
            Text('Employment: None'),
          ],
        ),
      )
    ]);
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: FadeInDemo(),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    MyApp(),
  );
}
{$ end main.dart $}

小结一下

Putting it all together

淡入文字效果 的例子展现了 AnimatedOpacity 的特性:

The Fade-in text effect example demonstrates the following features of AnimatedOpacity:

  • AnimatedOpacity 会监听其 opacity 属性的状态变化。

    AnimatedOpacity listens for state changes in its opacity property.

  • opacity 属性改变时, AnimatedOpacity 会自动将 opacity 变化到新值,同时使 widget 进行动画跟随变换。

    Whenever opacity changes, AnimatedOpacity automatically animates the widget’s transition to the new value for opacity.

  • AnimatedOpacity 需要一个 duration 参数来确定新旧 opacity 进行动画变换的时长。

    AnimatedOpacity requires a duration parameter to define the time it takes to animate the transition between an old opacity value and a new one.

示例:形状变化效果

Example: Shape-shifting effect

下面的示例将展示如何使用 AnimatedContainer widget 让多个不同类型(doubleColor)的属性(marginborderRadiuscolor)同时进行动画变换。

The following example shows how to use the AnimatedContainer widget to animate multiple properties (margin, borderRadius, and color) with different types (double and Color).

这个示例开始没有动画效果— 它以一个由 Material App 组成的主页面开始,有以下内容:

The example begins with no animation code—it starts with a Material App home screen that contains:

  • 一个有 marginborderRadius、和 color 属性的 Container,这些属性每次运行时的值都不同。

    A Container with borderRadius, margin, and color properties that are different each time you run the example.

  • 一个点击时什么都不做的 Change 按钮。

    A Change button that does nothing when clicked.

形状变化 (初始代码)

Shape-shifting (starter code)

点击 Run 按钮来运行这个示例:

Click the Run button to run the example:

{$ begin main.dart $}
import 'dart:math';

import 'package:flutter/material.dart';

double randomBorderRadius() {
  return Random().nextDouble() * 64;
}

double randomMargin() {
  return Random().nextDouble() * 64;
}

Color randomColor() {
  return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
}

class AnimatedContainerDemo extends StatefulWidget {
  _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  Color color;
  double borderRadius;
  double margin;

  @override
  initState() {
    color = randomColor();
    borderRadius = randomBorderRadius();
    margin = randomMargin();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            SizedBox(
              width: 128,
              height: 128,
              child: Container(
                margin: EdgeInsets.all(margin),
                decoration: BoxDecoration(
                  color: color,
                  borderRadius: BorderRadius.circular(borderRadius),
                ),
              ),
            ),
            ElevatedButton(
              child: Text('change'),
              onPressed: () => null,
            ),
          ],
        ),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: AnimatedContainerDemo(),
    );
  }
}

void main() {
  runApp(
    MyApp(),
  );
}
{$ end main.dart $}

使用 AnimatedContainer 将 color、borderRadius、和 margin 进行动画变换

Animate color, borderRadius, and margin with AnimatedContainer

这部分包含在 形状变化初始代码 中添加一个隐式动画的一系列步骤。完成这些步骤后,您还可以运行 形状变化完成代码,该代码已经实现了淡入效果。

This section contains a list of steps you can use to add an implicit animation to the shape-shifting starter code. After the steps, you can also run the shape-shifting complete example with the changes already made.

形状变化初始代码 中每个 Container widget 的属性(colorborderRadiusmargin)都由一个相关的函数赋值(分别是 randomColor()randomBorderRadius()randomMargin())。您可以使用 AnimatedContainer widget 重构这段代码,来完成以下的效果:

In the shape-shifting starter code, each property in the Container widget (color, borderRadius, and margin) is assigned a value by an associated function (randomColor(), randomBorderRadius(), and randomMargin() respectively). By using an AnimatedContainer widget, you can refactor this code to do the following:

  • 每当用户点击 Change 按钮时, colorborderRadiusmargin 都会生成一个新值。

    Generate new values for color, borderRadius, and margin whenever the user clicks the Change button.

  • 每当 colorborderRadiusmargin 被设置时,都会进行动画变换到新的值。

    Animate the transition to the new values for color, borderRadius, and margin whenever they are set.

1. 添加一个隐式动画

1. Add an implicit animation

Container widget 换成 AnimatedContainer widget:

Change the Container widget to an AnimatedContainer widget:

{container1 → container2}/lib/main.dart
@@ -44,7 +44,7 @@
44
44
  SizedBox(
45
45
  width: 128,
46
46
  height: 128,
47
- child: Container(
47
+ child: AnimatedContainer(
48
48
  margin: EdgeInsets.all(margin),
49
49
  decoration: BoxDecoration(
50
50
  color: color,

2. 为动画属性设置初始值

2. Set starting values for animated properties

当属性的新旧值发生变化时,AnimatedContainer 会自动在新旧值之间产生动画效果。通过创建一个 change() 方法,我们将定义当用户点击 Change 按钮时触发变更的行为。 change() 方法可以使用 setState()colorborderRadiusmargin 状态变量设置新值:

AnimatedContainer automatically animates between old and new values of its properties when they change. Create a change() method that defines the behavior triggered when the user clicks the Change button. The change() method can use setState() to set new values for the color, borderRadius, and margin state variables:

{container2 → container3}/lib/main.dart
@@ -35,6 +35,14 @@
35
35
  margin = randomMargin();
36
36
  }
37
+ void change() {
38
+ setState(() {
39
+ color = randomColor();
40
+ borderRadius = randomBorderRadius();
41
+ margin = randomMargin();
42
+ });
43
+ }
44
+
37
45
  @override
38
46
  Widget build(BuildContext context) {
39
47
  return Scaffold(

3. 为动画设置触发器

3. Set up a trigger for the animation

每当用户点击 Change 按钮时触发动画,调用 onPressed() 处理器的 change() 方法:

To set the animation to trigger whenever the user presses the Change button, invoke the change() method in the onPressed() handler:

{container3 → container4}/lib/main.dart
@@ -62,7 +62,7 @@
62
62
  ),
63
63
  ElevatedButton(
64
64
  child: Text('change'),
65
- onPressed: () => null,
65
+ onPressed: () => change(),
66
66
  ),
67
67
  ],
68
68
  ),

4. 设置时长

4. Set duration

在最后,设置新旧值之间变换的时长参数 duration

Finally, set the duration of the animation that powers the transition between the old and new values:

{container4 → container5}/lib/main.dart
@@ -6,6 +6,8 @@
6
6
  import 'package:flutter/material.dart';
7
+ const _duration = Duration(milliseconds: 400);
8
+
7
9
  double randomBorderRadius() {
8
10
  return Random().nextDouble() * 64;
9
11
  }
@@ -58,6 +60,7 @@
58
60
  color: color,
59
61
  borderRadius: BorderRadius.circular(borderRadius),
60
62
  ),
63
+ duration: _duration,
61
64
  ),
62
65
  ),
63
66
  ElevatedButton(

形状变化 (完成代码)

Shape-shifting (complete)

下面的示例是修改后的完成版代码— 运行这个示例,然后点击 Change 按钮就可以触发动画。注意:每次您点击 Change 按钮,形状的 marginborderRadius、和 color 都会进行动画变化到新的值。

Here’s the example with the completed changes you’ve made—run the code and click the Change button to trigger the animation. Notice that each time you click the Change button, the shape animates to its new values for margin, borderRadius, and color.

{$ begin main.dart $}
import 'dart:math';

import 'package:flutter/material.dart';

const _duration = Duration(milliseconds: 400);

double randomBorderRadius() {
  return Random().nextDouble() * 64;
}

double randomMargin() {
  return Random().nextDouble() * 64;
}

Color randomColor() {
  return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
}

class AnimatedContainerDemo extends StatefulWidget {
  _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  Color color;
  double borderRadius;
  double margin;

  @override
  void initState() {
    super.initState();
    color = Colors.deepPurple;
    borderRadius = randomBorderRadius();
    margin = randomMargin();
  }

  void change() {
    setState(() {
      color = randomColor();
      borderRadius = randomBorderRadius();
      margin = randomMargin();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            SizedBox(
              width: 128,
              height: 128,
              child: AnimatedContainer(
                margin: EdgeInsets.all(margin),
                decoration: BoxDecoration(
                  color: color,
                  borderRadius: BorderRadius.circular(borderRadius),
                ),
                duration: _duration,
              ),
            ),
            ElevatedButton(
              child: Text('change'),
              onPressed: () => change(),
            ),
          ],
        ),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: AnimatedContainerDemo(),
    );
  }
}

void main() {
  runApp(
    MyApp(),
  );
}
{$ end main.dart $}

使用动画曲线

Using animation curves

前面的示例展示出,如何让您通过隐式动画对特定的 widget 属性值进行动画变化,以及如何通过 duration 参数设置动画完成所需的时间。隐式动画还允许您在 duration 时长内控制动画的速率变化。用来定义这种速率变化的参数是 curve

The preceding examples show how implicit animations allow you to animate changes in values for specific widget properties, and how the duration parameter allows you to set the amount of time an animation takes to complete. Implicit animations also allow you to control changes to the rate of an animation within the duration. The parameter you use to define this change in rate is curve.

前面的例子中没有指定 curve,所以隐式动画默认使用 线性动画曲线。在 形状变化完成代码 中添加一个 curve 参数,然后当您将常量 easeInOutBack 传递给 curve 时,观察动画的变化:

The preceding examples do not specify a curve, so the implicit animations apply a linear animation curve by default. Add a curve parameter to the shape-shifting complete and watch how the animation changes when you pass the easeInOutBack constant for curve:

{container5 → container6}/lib/main.dart
@@ -61,6 +61,7 @@
61
61
  borderRadius: BorderRadius.circular(borderRadius),
62
62
  ),
63
63
  duration: _duration,
64
+ curve: Curves.easeInOutBack,
64
65
  ),
65
66
  ),
66
67
  ElevatedButton(

现在您已经将 easeInOutBack 作为 curve 的值传递给了 AnimatedContainer,注意:marginborderRadiuscolor 的变化速率遵循 easeInOutBack 所定义的曲线:

Now that you have passed easeInOutBack as the value for curve to AnimatedContainer, notice that the rates of change for margin, borderRadius, and color follow the curve defined by the easeInOutBack curve:

小结一下

Putting it all together

形状变化完成代码 示例对 marginborderRadiuscolor 属性值进行了动画变换。注意:AnimatedContainer 可以对它的任意属性进行动画改变,包括那些您没有使用的属性,比如 paddingtransform,甚至是 childalignment! 这个 形状变化完成代码 的示例建立在 渐变完成代码 的基础上,展现出隐式动画的额外功能:

The shape-shifting complete example animates transitions between values for margin, borderRadius, and color properties. Note that AnimatedContainer animates changes to any of its properties, including those you didn’t use such as padding, transform, and even child and alignment! The shape-shifting complete example builds upon fade-in complete by showing additional capabilities of implicit animations:

  • 一些隐式动画(比如 AnimatedOpacity)只能对一个属性值进行动画变换,然而有些(比如 AnimatedContainer)可以同时变换多个属性。

    Some implicit animations (for example, AnimatedOpacity) only animate a single property, while others (like AnimatedContainer) can animate many properties.

  • 隐式动画会在新旧属性值变换时,自动使用提供的 curveduration 进行动画变换。

    Implicit animations automatically animate between the old and new values of properties when they change using the provided curve and duration.

  • 如果您没有指定 curve,隐式动画的曲线会默认使用 线性曲线

    If you do not specify a curve, implicit animations default to a linear curve.

下一个是什么?

What’s next?

恭喜,您已经完成了这个 codelab!如果您想要了解更多,这里有一些其他文章的推荐:

Congratulations, you’ve finished the codelab! If you’d like to learn more, here are some suggestions for where to go next: