隱式動畫

歡迎來到隱式動畫的 codelab,在這裡你將學到:如何使用 Flutter widgets 輕鬆地對一組特定屬性建立動畫。

為了充分理解該 codelab,你應該具備以下基本知識:

該 codelab 包括以下內容:

  • 使用 AnimatedOpacity 來建立一個淡入效果。

  • 使用 AnimatedContainer 讓尺寸、顏色和邊距產生動畫變換。

  • 隱式動畫及其使用方法的概述。

完成該 codelab 的時間約為:15-30 分鐘。

什麼是隱式動畫?

透過使用 Flutter 的 動畫庫,你可以為 UI 中的元件新增運動和建立視覺效果。你可以使用庫中的一套元件來管理動畫,這些元件統稱為隱式動畫隱式動畫元件,其名稱源於它們都實現了 ImplicitlyAnimatedWidget 類。使用隱式動畫,你可以透過設定一個目標值,驅動 widget 的屬性進行動畫變換;每當目標值發生變化時,屬性會從舊值逐漸更新到新值。透過這種方式,隱式動畫內部實現了動畫控制,從而能夠方便地使用— 隱式動畫元件會管理動畫效果,使用者不需要再進行額外的處理。

範例:淡入文字效果

下面的範例展示了如何使用名為 AnimatedOpacity 的隱式動畫 widget,為已存在的 UI 新增淡入效果。 這個範例開始沒有動畫效果— 它包含一個由 Material App 組成的首頁面,有以下內容:

  • 一張貓頭鷹的照片。

  • 一個點選時什麼也不做的 Show details 按鈕。

  • 照片中貓頭鷹的描述文字。

淡入 (初始程式碼)

點選 Run 按鈕來執行這個範例:

// 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 widget 進行透明度動畫

這部分包含在 淡入初始程式碼 中新增一個隱式動畫一系列步驟。完成這些步驟後,你還可以執行 淡入完成程式碼,該程式碼已經實現了淡入效果。這些步驟概述瞭如何使用 AnimatedOpacity widget 來新增以下的動畫屬性:

  • 使用者點選 Show details 按鈕後,顯示貓頭鷹的描述文字。

  • 當用戶點選 Show details 按鈕時,貓頭鷹的描述文字淡入。

1. 選擇要進行動畫的 widget 屬性

想要建立淡入效果,你可以使用 AnimatedOpacity widget 對 opacity 屬性進行動畫。將 Column widget 換成 AnimatedOpacity widget:

{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 的初始值設定為 0 ,以便在使用者點選 Show details 前隱藏文字:

{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('Type: Owl'),

3. 為動畫設定一個時長

除了 opacity 引數以外,AnimatedOpacity 還需要為動畫設定 duration。在下面的例子中,動畫會以兩秒的時長執行:

{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. 為動畫設定一個觸發器,並選擇一個結束值

當用戶點選 Show details 按鈕時,將會觸發動畫。為了做到這點,我們使用 TextButtononPressed() 方法,在呼叫時改變 opacity 的狀態值為 1。

{opacity4 → opacity5}/lib/main.dart
@@ -27,7 +27,9 @@
27
27
  'Show Details',
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),

淡入 (完成程式碼)

下面的範例是修改後的完成版程式碼— 執行這個範例,然後點選 Show details 按鈕就可以觸發動畫。

// 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(),
  );
}

小結一下

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

  • AnimatedOpacity 會監聽其 opacity 屬性的狀態變化。

  • opacity 屬性改變時, AnimatedOpacity 會自動將 opacity 變化到新值,同時使 widget 進行動畫跟隨變換。

  • AnimatedOpacity 需要一個 duration 引數來確定新舊 opacity 進行動畫變換的時長。

範例:形狀變化效果

下面的範例將展示如何使用 AnimatedContainer widget 讓多個不同型別(doubleColor)的屬性(marginborderRadiuscolor)同時進行動畫變換。 這個範例開始沒有動畫效果— 它以一個由 Material App 組成的首頁面開始,有以下內容:

  • 一個有 marginborderRadius、和 color 屬性的 Container,這些屬性每次執行時的值都不同。

  • 一個點選時什麼都不做的 Change 按鈕。

形狀變化 (初始程式碼)

點選 Run 按鈕來執行這個範例:

// 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 將 color、borderRadius、和 margin 進行動畫變換

這部分包含在 形狀變化初始程式碼 中新增一個隱式動畫的一系列步驟。完成這些步驟後,你還可以執行 形狀變化範例,該程式碼已經實現了淡入效果。

形狀變化初始程式碼 中每個 Container widget 的屬性都由一個相關的函式賦值來完成以下的效果:

  • randomColor() 函式為 color 屬性生成新的 Color

  • randomBorderRadius() 函式為 borderRadius 屬性生成新的 double

  • randomMargin() 函式為 margin 屬性生成新的 double

以下步驟會使用 AnimatedContainer 來達到:

  • 每當使用者點選 Change 按鈕時, colorborderRadiusmargin 都會漸變到新的值。

  • 每當 colorborderRadiusmargin 被設定時,都會進行動畫變換到新的值。

1. 新增一個隱式動畫

Container widget 換成 AnimatedContainer widget:

{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 按鈕時觸發變更的行為。 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. 為動畫設定觸發器

每當使用者點選 Change 按鈕時觸發動畫,呼叫 onPressed() 處理器的 change() 方法:

{container3 → container4}/lib/main.dart
@@ -65,7 +65,7 @@
65
65
  ),
66
66
  ElevatedButton(
67
67
  child: const Text('Change'),
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(

形狀變化 (完成程式碼)

下面的範例是修改後的完成版程式碼— 執行這個範例,然後點選 Change 按鈕就可以觸發動畫。注意:每次你點選 Change 按鈕,形狀的 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(),
  );
}

使用動畫曲線

The preceding examples show how:

  • 如何讓你透過隱式動畫對特定的 widget 屬性值進行動畫變化。

  • 如何透過 duration 引數設定動畫完成所需的時間。

隱式動畫還允許你在 duration 時長內控制動畫的 速率 變化。用來定義這種速率變化的引數是 Curve,或者 Curves 這些已經預定義的曲線。

前面的例子中沒有指定 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!如果你想要瞭解更多,這裡有一些其他文章的推薦: