隐式动画

欢迎来到隐式动画 Codelab,您将学习如何使用 Flutter 小组件,这些小组件可以轻松为特定属性集创建动画。

为了充分利用此 Codelab,您应该具备以下基本知识

此 Codelab 涵盖以下内容

  • 使用 AnimatedOpacity 创建淡入效果。
  • 使用 AnimatedContainer 设置大小、颜色和边距的过渡动画。
  • 隐式动画和使用它们的技巧概述。

完成此代码实验室的估计时间:15-30 分钟。

什么是隐式动画?

使用 Flutter 的动画库,您可以为 UI 中的小部件添加动作并创建视觉效果。库中的一组小部件可为您管理动画。这些小部件统称为隐式动画隐式动画小部件,其名称源自它们实现的ImplicitlyAnimatedWidget类。使用隐式动画,您可以通过设置目标值来对小部件属性进行动画处理;每当该目标值发生更改时,小部件都会将属性从旧值动画处理到新值。通过这种方式,隐式动画以便利性换取控制权——它们管理动画效果,这样您就不必管理了。

示例:淡入文本效果

以下示例展示了如何使用名为AnimatedOpacity的隐式动画小部件,为现有 UI 添加淡入效果。此示例一开始没有任何动画代码——它包含一个Material App主屏幕,其中包含

  • 一张猫头鹰照片。
  • 一个显示详细信息按钮,单击后不执行任何操作。
  • 照片中猫头鹰的描述文本。

淡入(初始代码)

要查看示例,请单击运行

// Copyright 2019 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.

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({super.key});

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

class _FadeInDemoState extends State<FadeInDemo> {
  @override
  Widget build(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    return Column(children: <Widget>[
      Image.network(owlUrl, height: height * 0.8),
      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({super.key});

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

void main() {
  runApp(
    const MyApp(),
  );
}

使用 AnimatedOpacity 小部件设置不透明度动画

本部分包含可用于向淡入初始代码添加隐式动画的步骤列表。完成这些步骤后,您还可以运行淡入完成代码,其中已进行了更改。这些步骤概述了如何使用AnimatedOpacity小部件添加以下动画功能

  • 猫头鹰的描述文本在用户单击显示详细信息之前保持隐藏。
  • 当用户单击显示详细信息时,猫头鹰的描述文本淡入显示。

1. 选择要设置动画的小部件属性

要创建淡入效果,您可以使用AnimatedOpacity小部件设置opacity属性。将Column小部件包装在AnimatedOpacity小部件中

{opacity1 → opacity2}/lib/main.dart
@@ -27,12 +27,14 @@
27
27
  ),
28
28
  onPressed: () => {},
29
29
  ),
30
- const Column(
31
- children: [
32
- Text('Type: Owl'),
33
- Text('Age: 39'),
34
- Text('Employment: None'),
35
- ],
30
+ AnimatedOpacity(
31
+ child: const Column(
32
+ children: [
33
+ Text('Type: Owl'),
34
+ Text('Age: 39'),
35
+ Text('Employment: None'),
36
+ ],
37
+ ),
36
38
  )
37
39
  ]);
38
40
  }

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

要在用户点击显示详情之前隐藏文本,请将 opacity 的起始值设置为零

{opacity2 → opacity3}/lib/main.dart
@@ -15,6 +15,8 @@
15
15
  }
16
16
  class _FadeInDemoState extends State<FadeInDemo> {
17
+ double opacity = 0;
18
+
17
19
  @override
18
20
  Widget build(BuildContext context) {
19
21
  double height = MediaQuery.of(context).size.height;
@@ -28,6 +30,7 @@
28
30
  onPressed: () => {},
29
31
  ),
30
32
  AnimatedOpacity(
33
+ opacity: opacity,
31
34
  child: const Column(
32
35
  children: [
33
36
  Text('类型:猫头鹰'),

3. 设置动画持续时间

除了 opacity 参数,AnimatedOpacity 还需要一个 duration 用于动画。对于此示例,你可以从 2 秒开始

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

4. 设置动画触发器并选择结束值

配置动画,在用户点击显示详情时触发。为此,使用 TextButtononPressed() 处理程序更改 opacity 状态。若要使 FadeInDemo 小组件在用户点击显示详情时完全可见,请使用 onPressed() 处理程序将 opacity 设置为 1

{opacity4 → opacity5}/lib/main.dart
@@ -27,7 +27,9 @@
27
27
  '显示详情',
28
28
  style: TextStyle(color: Colors.blueAccent),
29
29
  ),
30
- onPressed: () => {},
30
+ onPressed: () => setState(() {
31
+ opacity = 1;
32
+ }),
31
33
  ),
32
34
  AnimatedOpacity(
33
35
  duration: const Duration(seconds: 2),

淡入(完成)

这是您已完成更改的示例。运行此示例,然后单击显示详情以触发动画。

// Copyright 2019 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.

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({super.key});

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

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

  @override
  Widget build(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    return Column(children: <Widget>[
      Image.network(owlUrl, height: height * 0.8),
      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({super.key});

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

void main() {
  runApp(
    const MyApp(),
  );
}

将所有内容放在一起

淡入文本效果示例演示了 AnimatedOpacity 小组件的以下功能。

  • 它侦听其 opacity 属性的状态更改。
  • opacity 属性更改时,它会为 opacity 的新值设置过渡动画。
  • 它需要一个 duration 参数来定义值之间的过渡需要多长时间。

示例:变形效果

以下示例展示了如何使用 AnimatedContainer 小组件为多种不同类型的属性(marginborderRadiuscolor)设置动画(doubleColor)。该示例最初不包含任何动画代码。它从包含 Material App 主屏幕开始,其中包含

  • 一个 Container 小部件,配置了 borderRadiusmargincolor。每次运行示例时,都会设置这些属性以重新生成。
  • 一个在点击时不执行任何操作的更改按钮。

形状变化(启动代码)

要启动示例,请点击运行

// Copyright 2019 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.

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({super.key});

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

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

  @override
  void 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({super.key});

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

void main() {
  runApp(
    const MyApp(),
  );
}

使用 AnimatedContainer 动画化颜色、borderRadius 和边距

本部分包含一个步骤列表,你可以使用它向 形状变化启动代码 添加一个隐式动画。完成每一步后,你还可以运行 完整的形状变化示例,其中已经进行了更改。

形状变化启动代码Container 小部件中的每个属性分配一个随机值。相关函数生成相关值

  • randomColor() 函数为 color 属性生成一个 Color
  • randomBorderRadius() 函数为 borderRadius 属性生成一个 double
  • randomMargin() 函数为 margin 属性生成一个 double

以下步骤使用 AnimatedContainer 小部件

  • 在用户点击更改时,过渡到 colorborderRadiusmargin 的新值。
  • 在设置 colorborderRadiusmargin 的新值时,动画化到这些新值的过渡。

1. 添加一个隐式动画

Container 小组件更改为 AnimatedContainer 小组件

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

2. 设置动画属性的起始值

AnimatedContainer 小组件的属性发生变化时,它会在其旧值和新值之间进行过渡。为了包含用户点击更改时触发的行为,创建一个 change() 方法。 change() 方法可以使用 setState() 方法为 colorborderRadiusmargin 状态变量设置新值

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

3. 设置动画触发器

要设置动画以便在用户按下更改时触发,请在 onPressed() 处理程序中调用 change() 方法

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

4. 设置持续时间

设置在旧值和新值之间转换的动画的 duration

{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
  }
@@ -61,6 +63,7 @@
61
63
  color: color,
62
64
  borderRadius: BorderRadius.circular(borderRadius),
63
65
  ),
66
+ duration: _duration,
64
67
  ),
65
68
  ),
66
69
  ElevatedButton(

形状转换(完成)

以下是您已完成更改的示例。运行代码并单击更改以触发动画。每次单击更改时,形状都会针对 marginborderRadiuscolor 的新值进行动画处理。

// Copyright 2019 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.

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({super.key});

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

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

  @override
  void 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({super.key});

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

void main() {
  runApp(
    const MyApp(),
  );
}

使用动画曲线

前面的示例展示了如何

  • 隐式动画允许您为特定小部件属性的值之间的转换设置动画。
  • duration 参数允许您设置动画完成所需的时间。

隐式动画还允许您控制在设置的 duration 期间发生的动画速率的变化。要定义此速率变化,请将 curve 参数的值设置为 Curve,例如在 Curves 类中声明的曲线。

前面的示例未为 curve 参数指定值。如果没有指定曲线值,隐式动画将应用 线性动画曲线

完整的形状转换示例 中为 curve 参数指定一个值。当您传递 curveeaseInOutBack 常量时,动画会发生变化,

{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(

Curves.easeInOutBack 常量传递给 AnimatedContainer 小部件的 curve 属性时,请观察 marginborderRadiuscolor 的变化速率如何遵循该常量定义的曲线。

将所有内容组合在一起

完整的形状转换示例会对 marginborderRadiuscolor 属性的值之间的过渡进行动画处理。AnimatedContainer 小部件会对其任何属性的更改进行动画处理。其中包括您未使用的属性,例如 paddingtransform,甚至 childalignment!通过展示隐式动画的其他功能,完整的形状转换示例建立在完整的淡入示例的基础上。

总结隐式动画

  • 某些隐式动画(如 AnimatedOpacity 小部件)仅对一个属性进行动画处理。其他动画(如 AnimatedContainer 小部件)可以对多个属性进行动画处理。
  • 隐式动画使用提供的 curveduration 在属性的旧值和新值之间对过渡进行动画处理。
  • 如果您未指定 curve,隐式动画会默认使用线性曲线

接下来是什么?

恭喜,您已完成代码实验室!要了解更多信息,请查看以下建议