隱式動畫
歡迎來到隱式動畫的 codelab,在這裡你將學到:如何使用 Flutter widgets 輕鬆地對一組特定屬性建立動畫。
為了充分理解該 codelab,你應該具備以下基本知識:
-
如何 建立一個 Flutter 應用。
-
如何使用 stateful widgets。
該 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:
@@ -27,12 +27,14 @@
|
|
27
27
|
),
|
28
28
|
onPressed: () => {},
|
29
29
|
),
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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 前隱藏文字:
@@ -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。在下面的例子中,動畫會以兩秒的時長執行:
@@ -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 按鈕時,將會觸發動畫。為了做到這點,我們使用 TextButton
的 onPressed()
方法,在呼叫時改變 opacity
的狀態值為 1。
@@ -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
讓多個不同型別(double
和 Color
)的屬性(margin
、borderRadius
和 color
)同時進行動畫變換。
這個範例開始沒有動畫效果—
它以一個由 Material App 組成的首頁面開始,有以下內容:
-
一個有
margin
、borderRadius
、和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 按鈕時,
color
、borderRadius
和margin
都會漸變到新的值。 -
每當
color
、borderRadius
和margin
被設定時,都會進行動畫變換到新的值。
1. 新增一個隱式動畫
將 Container
widget 換成 AnimatedContainer
widget:
@@ -47,7 +47,7 @@
|
|
47
47
|
SizedBox(
|
48
48
|
width: 128,
|
49
49
|
height: 128,
|
50
|
-
child:
|
50
|
+
child: AnimatedContainer(
|
51
51
|
margin: EdgeInsets.all(margin),
|
52
52
|
decoration: BoxDecoration(
|
53
53
|
color: color,
|
2. 為動畫屬性設定初始值
當屬性的新舊值發生變化時,AnimatedContainer
會自動在新舊值之間產生動畫效果。透過建立一個 change()
方法,我們將定義當用戶點選 Change 按鈕時觸發變更的行為。
change()
方法可以使用 setState()
為 color
、borderRadius
和 margin
狀態變數設定新值:
@@ -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()
方法:
@@ -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
:
@@ -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 按鈕,形狀的 margin
、borderRadius
和 color
都會進行動畫變化到新的值。
// 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
時,觀察動畫的變化:
@@ -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
,注意:margin
、borderRadius
和 color
的變化速率遵循 easeInOutBack
所定義的曲線:
小結一下
形狀變化範例
對 margin
、borderRadius
和 color
屬性值進行了動畫變換。注意:AnimatedContainer
可以對它的任意屬性進行動畫改變,包括那些你沒有使用的屬性,比如 padding
、transform
,甚至是 child
和 alignment
!
這個 形狀變化範例
建立在 漸變完成程式碼 的基礎上,展現出隱式動畫的額外功能。
總結隱式動畫的特點:
-
一些隱式動畫(比如
AnimatedOpacity
)只能對一個屬性值進行動畫變換,然而有些(比如AnimatedContainer
)可以同時變換多個屬性。 -
隱式動畫會在新舊屬性值變換時,自動使用提供的
curve
和duration
進行動畫變換。 -
如果你沒有指定
curve
,隱式動畫的曲線會預設使用 線性曲線。
下一步是什麼?
恭喜,你已經完成了這個 codelab!如果你想要瞭解更多,這裡有一些其他文章的推薦:
-
嘗試一下 動畫教程。
-
學習 hero 動畫 和 staggered 動畫。
-
檢視更多 動畫庫 的訊息。
-
嘗試一下其他的 codelab。