隱式動畫
歡迎來到隱式動畫教學 (codelab),在這裡你將學習如何使用 Flutter 元件 (Widgets) 來輕鬆為特定屬性建立動畫效果。
為了讓你能從本教學獲得最大效益,建議你先具備以下基礎知識:
- 如何[建立一個 Flutter 應用程式][make a Flutter app]。
- 如何使用[有狀態元件 (Stateful Widgets)][stateful widgets]。
本教學涵蓋以下內容:
- 使用
AnimatedOpacity建立淡入 (fade-in) 效果。 - 使用
AnimatedContainer動畫化尺寸、顏色與邊距的轉換。 - 隱式動畫的概述與使用技巧。
完成本教學的預估時間:15-30 分鐘。
什麼是隱式動畫?
#透過 Flutter 的[動畫函式庫 (animation library)][animation library], 你可以為 UI 中的元件 (Widgets) 增加動態效果與視覺特效。 函式庫中有一組元件 (Widgets) 會自動為你管理動畫, 這些元件統稱為 隱式動畫 (implicit animations), 或稱 隱式動畫元件 (implicitly animated widgets), 名稱來自它們所實作的 [ImplicitlyAnimatedWidget][ImplicitlyAnimatedWidget] 類別。 使用隱式動畫時, 你只需設定目標屬性值即可讓元件自動產生動畫效果; 每當目標值改變時, 元件會自動將屬性從舊值動畫到新值。 因此,隱式動畫以便利性換取控制權—它們會自動管理動畫效果,讓你無需自行處理。
範例:文字淡入效果
#以下範例說明如何利用名為 [AnimatedOpacity][AnimatedOpacity] 的隱式動畫元件 (Widget) 為現有 UI 加入淡入效果。 此範例一開始並未包含任何動畫程式碼—它 僅包含一個 [Material App][Material App] 首頁畫面,內容包括:
- 一張貓頭鷹的照片。
- 一個 顯示詳細資料 按鈕,點擊時不會有任何動作。
- 照片中貓頭鷹的描述文字。
淡入效果(起始程式碼)
#點選 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/content/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) {
return ListView(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({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: FadeInDemo(),
),
),
);
}
}
void main() {
runApp(
const MyApp(),
);
}
使用 AnimatedOpacity 元件動畫化透明度
#本節將列出一系列步驟,協助你在 [淡入效果起始程式碼][fade-in starter code] 中加入隱式動畫。 步驟說明後,你也可以直接執行 [淡入效果完成版][fade-in complete] 程式碼,體驗已完成的效果。 這些步驟將指導你如何使用 AnimatedOpacity 元件 (Widget) 加入下列動畫功能:
- 貓頭鷹的描述文字在使用者點擊 顯示詳細資料 前保持隱藏。
- 當使用者點擊 顯示詳細資料 時, 貓頭鷹的描述文字會以淡入方式顯示。
1. 選擇要動畫化的元件屬性
#若要建立淡入效果,你可以利用 opacity 屬性,並搭配 AnimatedOpacity 元件 (Widget) 來實現。 將 Column 元件包裹在 AnimatedOpacity 元件中:
@override
Widget build(BuildContext context) {
return ListView(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'),
],
),
AnimatedOpacity(
child: const Column(
children: [
Text('Type: Owl'),
Text('Age: 39'),
Text('Employment: None'),
],
),
),
]);
}2. 初始化動畫屬性的狀態變數
#為了在使用者點擊 Show details 之前隱藏文字,請將 opacity 的起始值設為零:
class _FadeInDemoState extends State<FadeInDemo> {
double opacity = 0;
@override
Widget build(BuildContext context) {
return ListView(children: <Widget>[
// ...
AnimatedOpacity(
opacity: opacity,
child: const Column(3. 設定動畫(Animation)的持續時間
#除了 opacity 參數之外,AnimatedOpacity 還需要指定一個 [duration][duration],以用於其動畫(Animation)。在這個範例中, 你可以先設定為 2 秒:
AnimatedOpacity(
duration: const Duration(seconds: 2),
opacity: opacity,
child: const Column(4. 設定動畫(Animation)的觸發條件並選擇結束值
#將動畫(Animation)設定為在使用者點擊 Show details 時觸發。 為此,請使用 onPressed() 處理函式來變更 opacity 狀態, 並將其綁定到 TextButton。 為了讓 FadeInDemo 元件(Widget)在使用者點擊 Show details 時完全顯示, 請在 onPressed() 處理函式中將 opacity 設為 1:
TextButton(
child: const Text(
'Show Details',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => {},
onPressed: () => setState(() {
opacity = 1;
}),
),淡入效果(完成版)
#以下是你已完成修改後的範例。 執行此範例,然後點擊 Show details 以觸發動畫(Animation)。
// 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/content/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) {
return ListView(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({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: FadeInDemo(),
),
),
);
}
}
void main() {
runApp(
const MyApp(),
);
}
整合所有內容
#[淡入文字效果][Fade-in text effect] 範例展示了 AnimatedOpacity 元件(Widget)的以下特性:
- 監聽其
opacity屬性的狀態變化。 - 當
opacity屬性變更時, 會針對opacity的新值進行動畫(Animation)過渡。 - 需要一個
duration參數來定義 值之間過渡所需的時間。
範例:變形(Shape-shifting)效果
#以下範例展示如何使用 [AnimatedContainer][AnimatedContainer] 元件(Widget), 同時針對多個屬性(margin、borderRadius 和 color) 以及不同型別(double 和 Color)進行動畫(Animation)。 此範例一開始沒有任何動畫(Animation)程式碼。 它從一個 [Material App][Material App] 首頁開始,內容包含:
- 一個已設定
borderRadius、margin和color的Container元件(Widget)。 這些屬性會在每次執行範例時重新產生。 - 一個 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
#本節提供一系列步驟,協助你將隱式動畫(Implicit animation)加入 [變形效果起始程式碼][shape-shifting starter code]。 每完成一個步驟,也可以直接執行 [完整變形效果範例][complete shape-shifting example],檢視已套用變更的結果。
[變形效果起始程式碼][shape-shifting starter code] 會為 Container 元件(Widget)中的每個屬性指派一個隨機值。 相關函式會產生對應的值:
randomColor()函式會為color屬性產生ColorrandomBorderRadius()函式會為borderRadius屬性產生doublerandomMargin()函式會為margin屬性產生double
下列步驟將使用 AnimatedContainer 元件(Widget)來:
- 每當使用者點擊 Change 時,將
color、borderRadius和margin過渡到新值。 - 每當
color、borderRadius和margin被設定時,對其進行動畫(Animation)過渡。
1. 新增隱式動畫(Implicit animation)
#將 Container 元件(Widget)更改為 AnimatedContainer 元件(Widget):
SizedBox(
width: 128,
height: 128,
child: Container(
child: AnimatedContainer(
margin: EdgeInsets.all(margin),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(borderRadius),
),
),
),2. 設定動畫屬性的起始值
#當AnimatedContainer元件(Widget)的屬性發生變化時,會在舊值與新值之間進行過渡動畫。
為了包覆當使用者點擊 Change 時所觸發的行為,請建立一個change()方法。
change()方法可以使用setState()方法來設定color、borderRadius和margin這三個 state 變數的新值:
void change() {
setState(() {
color = randomColor();
borderRadius = randomBorderRadius();
margin = randomMargin();
});
}
@override
Widget build(BuildContext context) {
// ...3. 設定動畫 (Animation) 的觸發條件
#若要在使用者按下 Change 時觸發動畫 (Animation),請在 onPressed() 處理函式中呼叫 change() 方法:
ElevatedButton(
child: const Text('Change'),
onPressed: () => {},
onPressed: () => change(),
),4. 設定動畫(Animation)持續時間
#設定duration,以決定動畫(Animation)在舊值與新值之間轉換時所需的時間:
SizedBox(
width: 128,
height: 128,
child: AnimatedContainer(
margin: EdgeInsets.all(margin),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(borderRadius),
),
duration: const Duration(milliseconds: 400),
),
),形狀變換(完成版)
#以下是你已完成修改後的範例。 執行程式碼並點擊 Change 來觸發動畫(Animation)。 每次點擊 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(),
);
}
使用動畫曲線(animation curves)
#前述範例展示了:
- 隱式動畫(implicit animations)允許你針對特定元件(Widget)屬性的值變化進行動畫過渡。
duration參數可用來設定動畫完成所需的時間長度。
隱式動畫同時也允許你控制動畫在設定的 duration 期間內速率的變化。 若要定義這個速率變化, 請將 curve 參數設為 [Curve][Curve],例如在 [Curves][Curves] 類別中所宣告的曲線。
前述範例並未為 curve 參數指定值。 若未指定曲線(curve)值, 隱式動畫會套用 [線性動畫曲線(linear animation curve)][linear animation curve]。
你可以在 [完整的形狀變換範例][complete shape-shifting example] 中,為 curve 參數指定值。 當你將 [easeInOutBack][easeInOutBack] 常數傳遞給 curve 時,動畫會產生變化。
SizedBox(
width: 128,
height: 128,
child: AnimatedContainer(
margin: EdgeInsets.all(margin),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(borderRadius),
),
duration: _duration,
curve: Curves.easeInOutBack,
),
),當你將 Curves.easeInOutBack 常數傳遞給 curve 元件的 AnimatedContainer 屬性時,請觀察 margin、borderRadius 和 color 的變化速率如何遵循該常數所定義的曲線。
整合所有內容
#[完整的形狀變換範例][complete shape-shifting example] 會對 margin、borderRadius 和 color 屬性的值之間的轉換進行動畫處理。 AnimatedContainer 元件可以對其任何屬性的變化進行動畫處理, 包括你未使用到的屬性,例如 padding、transform, 甚至還有 child 和 alignment! 透過展示隱式動畫的更多功能, [完整的形狀變換範例][complete shape-shifting example] 是在 [fade-in complete][fade-in complete] 範例的基礎上進一步延伸。
總結隱式動畫:
- 有些隱式動畫元件,如
AnimatedOpacity,只會針對一個屬性進行動畫處理。 其他像AnimatedContainer這類元件,則可以同時對多個屬性進行動畫處理。 - 隱式動畫會在屬性值發生變化時,根據所提供的
curve和duration,對新舊值之間的轉換進行動畫處理。 - 如果你沒有指定
curve,隱式動畫會預設使用 [線性曲線][linear curve]。
接下來呢?
#恭喜你完成了本次 codelab! 若想進一步學習,請參考以下建議:
- 嘗試 [animations tutorial][animations tutorial]。
- 了解 [hero animations][hero animations] 以及 [staggered animations][staggered animations]。
- 查看 [animation library][animation library]。
- 探索其他 [Flutter learning resources][Flutter learning resources]。