隐式动画

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

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

该 codelab 包括以下内容:

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

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

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

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

什么是隐式动画?

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

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

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

示例:淡入文字效果

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

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

  • 一张猫头鹰的照片。

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

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

淡入 (初始代码)

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

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

const owlUrl =
    'https://raw.githubusercontent.com/flutter/website/main/src/assets/images/docs/owl.jpg';

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

  @override
  State<FadeInDemo> createState() => _FadeInDemoState();
}

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: FadeInDemo(),
        ),
      ),
    );
  }
}

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

使用 AnimatedOpacity widget 进行透明度动画

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

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

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

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

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

{opacity1 → opacity2}/lib/main.dart
@@ -2,6 +2,8 @@
2
2
  // Use of this source code is governed by a BSD-style license
3
3
  // that can be found in the LICENSE file.
4
+ // ignore_for_file: missing_required_argument
5
+
4
6
  import 'package:flutter/material.dart';
5
7
  const owlUrl =
@@ -25,12 +27,14 @@
25
27
  style: TextStyle(color: Colors.blueAccent),
26
28
  ),
27
29
  onPressed: () => {}),
28
- const Column(
29
- children: [
30
- Text('Type: Owl'),
31
- Text('Age: 39'),
32
- Text('Employment: None'),
33
- ],
30
+ AnimatedOpacity(
31
+ child: const Column(
32
+ children: [
33
+ Text('Type: Owl'),
34
+ Text('Age: 39'),
35
+ Text('Employment: None'),
36
+ ],
37
+ ),
34
38
  )
35
39
  ]);
36
40
  }

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

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

{opacity2 → opacity3}/lib/main.dart
@@ -2,8 +2,6 @@
2
2
  // Use of this source code is governed by a BSD-style license
3
3
  // that can be found in the LICENSE file.
4
- // ignore_for_file: missing_required_argument
5
-
6
4
  import 'package:flutter/material.dart';
7
5
  const owlUrl =
@@ -17,6 +15,8 @@
17
15
  }
18
16
  class _FadeInDemoState extends State<FadeInDemo> {
17
+ double opacity = 0.0;
18
+
19
19
  @override
20
20
  Widget build(BuildContext context) {
21
21
  return Column(children: <Widget>[
@@ -28,6 +28,8 @@
28
28
  ),
29
29
  onPressed: () => {}),
30
30
  AnimatedOpacity(
31
+ duration: const Duration(seconds: 3),
32
+ opacity: opacity,
31
33
  child: const Column(
32
34
  children: [
33
35
  Text('Type: Owl'),

3. 为动画设置一个时长

除了 opacity 参数以外,AnimatedOpacity 还需要为动画设置 duration。在下面的例子中,动画会以两秒的时长运行:

{opacity3 → opacity4}/lib/main.dart
@@ -28,7 +28,7 @@
28
28
  ),
29
29
  onPressed: () => {}),
30
30
  AnimatedOpacity(
31
- duration: const Duration(seconds: 3),
31
+ duration: const Duration(seconds: 2),
32
32
  opacity: opacity,
33
33
  child: const Column(
34
34
  children: [

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

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

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

淡入 (完成代码)

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

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

const owlUrl =
    'https://raw.githubusercontent.com/flutter/website/main/src/assets/images/docs/owl.jpg';

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

  @override
  State<FadeInDemo> createState() => _FadeInDemoState();
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: FadeInDemo(),
        ),
      ),
    );
  }
}

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

小结一下

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

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

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

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

示例:形状变化效果

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

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

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

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

形状变化 (初始代码)

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

{$ 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 {
  const AnimatedContainerDemo({Key? key}) : super(key: key);

  @override
  State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}

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

  @override
  initState() {
    super.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: const Text('change'),
              onPressed: () => {},
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: AnimatedContainerDemo(),
    );
  }
}

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

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

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

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

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

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

1. 添加一个隐式动画

Container widget 换成 AnimatedContainer widget:

{container1 → container2}/lib/main.dart
@@ -2,6 +2,8 @@
2
2
  // Use of this source code is governed by a BSD-style license
3
3
  // that can be found in the LICENSE file.
4
+ // ignore_for_file: missing_required_argument
5
+
4
6
  import 'dart:math';
5
7
  import 'package:flutter/material.dart';
@@ -47,7 +49,7 @@
47
49
  SizedBox(
48
50
  width: 128,
49
51
  height: 128,
50
- child: Container(
52
+ child: AnimatedContainer(
51
53
  margin: EdgeInsets.all(margin),
52
54
  decoration: BoxDecoration(
53
55
  color: color,

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

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

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

3. 为动画设置触发器

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

{container3 → container4}/lib/main.dart
@@ -67,7 +67,7 @@
67
67
  ),
68
68
  ElevatedButton(
69
69
  child: const Text('change'),
70
- onPressed: () => {},
70
+ onPressed: () => change(),
71
71
  ),
72
72
  ],
73
73
  ),

4. 设置时长

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

{container4 → container5}/lib/main.dart
@@ -2,12 +2,12 @@
2
2
  // Use of this source code is governed by a BSD-style license
3
3
  // that can be found in the LICENSE file.
4
- // ignore_for_file: missing_required_argument
5
-
6
4
  import 'dart:math';
7
5
  import 'package:flutter/material.dart';
6
+ const _duration = Duration(milliseconds: 400);
7
+
8
8
  double randomBorderRadius() {
9
9
  return Random().nextDouble() * 64;
10
10
  }
@@ -63,6 +63,7 @@
63
63
  color: color,
64
64
  borderRadius: BorderRadius.circular(borderRadius),
65
65
  ),
66
+ duration: _duration,
66
67
  ),
67
68
  ),
68
69
  ElevatedButton(

形状变化 (完成代码)

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

{$ 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 {
  const AnimatedContainerDemo({Key? key}) : super(key: key);

  @override
  State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}

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

  @override
  initState() {
    super.initState();
    color = randomColor();
    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: const Text('change'),
              onPressed: () => change(),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: AnimatedContainerDemo(),
    );
  }
}

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

使用动画曲线

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

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

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

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

小结一下

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

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

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

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

下一个是什么?

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