Hero 動畫
您可能已經看過許多次英雄動畫。例如,螢幕會顯示代表待售商品的縮圖清單。選擇一個商品會將它飛到一個新的畫面,其中包含更多詳細資料和「購買」按鈕。將圖片從一個畫面飛到另一個畫面在 Flutter 中稱為英雄動畫,儘管相同的動作有時會稱為共享元素轉場。
您可能想觀看這段一分鐘的影片,介紹 Hero Widget
本指南示範如何建立標準英雄動畫,以及在飛行過程中將影像從圓形轉換為方形的英雄動畫。
路由描述 Flutter 應用程式中的頁面或螢幕。
您可以使用 Hero Widget 在 Flutter 中建立此動畫。當英雄從來源路由動畫到目的地路由時,目的地路由 (不包含英雄) 會淡入畫面。通常,英雄是使用者介面的小部分,例如兩個路由共有的影像。從使用者的角度來看,英雄會在路由之間「飛行」。本指南說明如何建立下列英雄動畫
標準英雄動畫
標準英雄動畫會將英雄從一個路由飛到新的路由,通常會落在不同的位置並具有不同的尺寸。
以下影片 (以慢速錄製) 顯示了一個典型的範例。點擊路由中心的鰭片會將它們飛到新的藍色路由的左上角,尺寸較小。點擊藍色路由中的鰭片 (或使用裝置的返回上一個路由手勢) 會將鰭片飛回原始路由。
放射狀英雄動畫
在放射狀英雄動畫中,當英雄在路由之間飛行時,其形狀似乎會從圓形變為矩形。
以下影片 (以慢速錄製) 顯示了放射狀英雄動畫的範例。在開始時,會在路由底部顯示一排三個圓形影像。點擊任何圓形影像會將該影像飛到新的路由,該路由會以方形顯示它。點擊方形影像會將英雄飛回原始路由,並以圓形顯示。
在移動到 標準 或 放射狀 英雄動畫的特定章節之前,請閱讀 英雄動畫的基本結構,了解如何建構英雄動畫程式碼,以及 幕後花絮,了解 Flutter 如何執行英雄動畫。
英雄動畫的基本結構
#- 在不同的路由中使用兩個具有相符標籤的英雄 Widget 來實作動畫。
- Navigator 管理包含應用程式路由的堆疊。
- 將路由推送到 Navigator 的堆疊或從中彈出路由會觸發動畫。
- Flutter 框架會計算矩形補間動畫,
RectTween
,它定義了英雄從來源路由飛到目的地路由時的邊界。在飛行期間,英雄會被移動到應用程式覆蓋層,以便它會顯示在兩個路由的頂端。
如果您對補間或補間的概念不熟悉,請查看Flutter 動畫教學課程。
英雄動畫是使用兩個 Hero
Widget 實作的:一個描述來源路由中的 Widget,另一個描述目的地路由中的 Widget。從使用者的角度來看,英雄似乎是共享的,只有程式設計師需要了解此實作細節。英雄動畫程式碼具有下列結構
- 定義一個起始的 Hero Widget,稱為來源英雄。英雄指定其圖形表示 (通常是影像) 和識別標籤,並且在來源路由定義的目前顯示 Widget 樹狀結構中。
- 定義一個結束的 Hero Widget,稱為目的地英雄。此英雄也會指定其圖形表示,以及與來源英雄相同的標籤。必須使用相同的標籤建立兩個英雄 Widget,通常是表示基礎資料的物件。為了獲得最佳效果,英雄應具有幾乎相同的 Widget 樹狀結構。
- 建立包含目的地英雄的路由。目的地路由會定義動畫結束時存在的 Widget 樹狀結構。
- 藉由將目的地路由推送到 Navigator 的堆疊上來觸發動畫。Navigator 的推送和彈出操作會為來源和目的地路由中每對具有相符標籤的英雄觸發英雄動畫。
Flutter 會計算補間動畫,使 Hero 的邊界從起點動畫到終點 (插補大小和位置),並在覆蓋層中執行動畫。
下一節將更詳細地描述 Flutter 的過程。
幕後花絮
#以下說明 Flutter 如何執行從一個路由到另一個路由的轉換。
在轉換之前,來源英雄會在來源路由的 Widget 樹狀結構中等待。目的地路由尚不存在,且覆蓋層是空的。
將路由推送到 Navigator
會觸發動畫。在 t=0.0
時,Flutter 會執行下列動作
使用 Material 動態規格中描述的彎曲動作,在螢幕外計算目的地英雄的路徑。Flutter 現在知道英雄的最終位置。
將目的地英雄放置在覆蓋層中,位置和尺寸與來源英雄相同。將英雄新增到覆蓋層會變更其 Z 順序,使其顯示在所有路由的頂端。
將來源英雄移到螢幕外。
當英雄飛行時,其矩形邊界會使用 Tween<Rect> 進行動畫處理,並在 Hero 的 createRectTween
屬性中指定。預設情況下,Flutter 會使用 MaterialRectArcTween
的實例,它會沿著彎曲路徑為矩形的對角進行動畫處理。(如需使用不同補間動畫的範例,請參閱放射狀英雄動畫。)
當飛行完成時
Flutter 會將英雄 Widget 從覆蓋層移到目的地路由。覆蓋層現在是空的。
目的地英雄會以其在目的地路由中的最終位置顯示。
來源英雄會還原到其路由。
彈出路由會執行相同的過程,使英雄動畫回到其在來源路由中的大小和位置。
重要類別
#本指南中的範例使用下列類別來實作英雄動畫
Hero
- Hero
InkWell
- 指定點擊英雄時會發生的動作。
InkWell
的onTap()
方法會建立新的路由並將其推送到Navigator
的堆疊。 Navigator
Navigator
會管理路由堆疊。將路由推送到Navigator
的堆疊或從中彈出路由會觸發動畫。Route
- 指定螢幕或頁面。大多數應用程式 (超出最基本應用程式) 都具有多個路由。
標準英雄動畫
#- 使用
MaterialPageRoute
、CupertinoPageRoute
指定路由,或使用PageRouteBuilder
建立自訂路由。本節中的範例使用 MaterialPageRoute。 - 藉由將目的地的影像包裝在
SizedBox
中,變更轉換結束時的影像大小。 - 藉由將目的地的影像放置在版面配置 Widget 中,變更影像的位置。這些範例使用
Container
。
下列每個範例都會示範將影像從一個路由飛到另一個路由。本指南說明第一個範例。
- hero_animation
- 將英雄程式碼封裝在自訂的
PhotoHero
Widget 中。沿著彎曲路徑為英雄的移動進行動畫處理,如 Material 動態規格中所述。 - basic_hero_animation
- 直接使用英雄 Widget。此更基本的範例僅供您參考,本指南中沒有說明。
發生了什麼事?
#使用 Flutter 的英雄 Widget 可以輕鬆實作將影像從一個路由飛到另一個路由的功能。當使用 MaterialPageRoute
指定新的路由時,影像會沿著彎曲的路徑飛行,如 Material Design 動態規格中所述。
建立新的 Flutter 範例並使用來自 hero_animation 的檔案進行更新。
若要執行此範例
- 點擊首頁路由的照片,將影像飛到新的路由,其中會以不同的位置和比例顯示相同的照片。
- 點擊影像或使用裝置的返回上一個路由手勢,返回上一個路由。
- 您可以使用
timeDilation
屬性進一步減慢轉換速度。
PhotoHero 類別
#自訂的 PhotoHero 類別會維護英雄,以及其大小、影像和點擊時的行為。PhotoHero 會建構下列 Widget 樹狀結構
以下是程式碼
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.width,
});
final String photo;
final VoidCallback? onTap;
final double width;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
);
}
}
重要資訊
- 當
HeroAnimation
作為應用程式的 home 屬性提供時,起始路由會由MaterialApp
隱式地推送。 InkWell
包裹著圖片,使得在來源和目標的 hero 上添加點擊手勢變得非常簡單。- 使用透明顏色定義 Material 小部件,可以使圖片在飛向目的地時從背景中「彈出」。
SizedBox
指定了動畫開始和結束時 hero 的大小。- 將圖片的
fit
屬性設定為BoxFit.contain
,可確保圖片在過渡期間盡可能放大,同時不改變其長寬比。
HeroAnimation 類別
#HeroAnimation
類別會建立來源和目標的 PhotoHero,並設定過渡效果。
以下是程式碼
class HeroAnimation extends StatelessWidget {
const HeroAnimation({super.key});
Widget build(BuildContext context) {
timeDilation = 5.0; // 1.0 means normal animation speed.
return Scaffold(
appBar: AppBar(
title: const Text('Basic Hero Animation'),
),
body: Center(
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 300.0,
onTap: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
// Set background to blue to emphasize that it's a new route.
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 100.0,
onTap: () {
Navigator.of(context).pop();
},
),
),
);
}
));
},
),
),
);
}
}
重要資訊
- 當使用者點擊包含來源 hero 的
InkWell
時,程式碼會使用MaterialPageRoute
建立目標路由。將目標路由推送到Navigator
的堆疊會觸發動畫。 Container
將PhotoHero
定位在目標路由左上角、AppBar
的下方。- 目標
PhotoHero
的onTap()
方法會彈出Navigator
的堆疊,觸發動畫將Hero
飛回原始路由。 - 在偵錯時,使用
timeDilation
屬性來減慢過渡速度。
放射狀英雄動畫
#- 徑向轉換會將圓形形狀動畫化為方形形狀。
- 徑向 hero 動畫會在 hero 從來源路由飛到目標路由時執行徑向轉換。
- MaterialRectCenterArcTween 定義了補間動畫。
- 使用
PageRouteBuilder
建立目標路由。
當 hero 從圓形轉換為矩形時,將其從一個路由飛到另一個路由是一個很棒的效果,您可以使用 Hero 小部件來實作。為此,程式碼會將兩個剪輯形狀的交集動畫化:一個圓形和一個方形。在整個動畫過程中,圓形剪輯(以及圖片)會從 minRadius
縮放到 maxRadius
,而方形剪輯則保持恆定大小。同時,圖片會從來源路由中的位置飛到目標路由中的位置。如需此轉換的視覺範例,請參閱 Material 動態規格中的徑向轉換。
這個動畫可能看起來很複雜(而且確實如此),但您可以自訂提供的範例以滿足您的需求。繁重的工作已為您完成。
以下每個範例都展示了徑向 hero 動畫。本指南將描述第一個範例。
- radial_hero_animation
- 如 Material 動態規格所述的徑向 hero 動畫。
- basic_radial_hero_animation
- 徑向 hero 動畫的最簡單範例。目標路由沒有 Scaffold、Card、Column 或 Text。此基本範例僅供您參考,本指南中不作描述。
- radial_hero_animation_animate
_rectclip - 透過動畫化矩形剪輯的大小來擴展 radial_hero_animation。此更進階的範例僅供您參考,本指南中不作描述。
徑向 hero 動畫涉及將圓形與方形相交。即使使用 timeDilation
減慢動畫速度,也可能很難看到,因此您可能會考慮在開發期間啟用 debugPaintSizeEnabled
標誌。
發生了什麼事?
#下圖顯示了動畫開始時 (t = 0.0
) 和結束時 (t = 1.0
) 的剪裁圖片。
藍色漸層(代表圖片)表示剪輯形狀的相交位置。在過渡開始時,交集的結果是圓形剪輯 (ClipOval
)。在轉換期間,ClipOval
會從 minRadius
縮放到 maxRadius
,而 ClipRect 則保持恆定大小。在過渡結束時,圓形和矩形剪輯的交集會產生一個與 hero 小部件大小相同的矩形。換句話說,在過渡結束時,圖片不再被剪裁。
建立新的 Flutter 範例,並使用 radial_hero_animation GitHub 目錄中的檔案更新它。
若要執行此範例
- 點擊三個圓形縮圖之一,以將圖片動畫化到新路由中間的較大方形,該路由會遮擋原始路由。
- 點擊影像或使用裝置的返回上一個路由手勢,返回上一個路由。
- 您可以使用
timeDilation
屬性進一步減慢轉換速度。
Photo 類別
#Photo
類別會建立保存圖片的小部件樹狀結構
class Photo extends StatelessWidget {
const Photo({super.key, required this.photo, this.color, this.onTap});
final String photo;
final Color? color;
final VoidCallback onTap;
Widget build(BuildContext context) {
return Material(
// Slightly opaque color appears where the image has transparency.
color: Theme.of(context).primaryColor.withOpacity(0.25),
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
);
}
}
重要資訊
InkWell
會捕捉點擊手勢。呼叫函式會將onTap()
函式傳遞給Photo
的建構函式。- 在飛行過程中,
InkWell
會在其第一個 Material 祖先上繪製其水波紋效果。 - Material 小部件具有稍微不透明的顏色,因此圖片的透明部分會以顏色呈現。這確保了即使對於具有透明度的圖片,也很容易看到圓形到方形的轉換。
Photo
類別在其小部件樹狀結構中不包含Hero
。為了使動畫生效,hero 會包裹RadialExpansion
小部件。
RadialExpansion 類別
#RadialExpansion
小部件是示範的核心,會建立在過渡期間剪裁圖片的小部件樹狀結構。剪裁的形狀是圓形剪輯(在飛行過程中增大)與矩形剪輯(在整個過程中保持恆定大小)的交集產生的結果。
為此,它會建立以下小部件樹狀結構
以下是程式碼
class RadialExpansion extends StatelessWidget {
const RadialExpansion({
super.key,
required this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
final double maxRadius;
final clipRectSize;
final Widget child;
@override
Widget build(BuildContext context) {
return ClipOval(
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect(
child: child, // Photo
),
),
),
);
}
}
重要資訊
hero 會包裹
RadialExpansion
小部件。當 hero 飛行時,其大小會改變,並且由於它會限制其子項的大小,因此
RadialExpansion
小部件也會改變大小以匹配。RadialExpansion
動畫由兩個重疊的剪輯建立。範例使用
MaterialRectCenterArcTween
定義補間插值。hero 動畫的預設飛行路徑會使用 hero 的角落來插值補間。此方法會影響徑向轉換期間 hero 的長寬比,因此新的飛行路徑會使用MaterialRectCenterArcTween
來使用每個 hero 的中心點插值補間。以下是程式碼
dartstatic RectTween _createRectTween(Rect? begin, Rect? end) { return MaterialRectCenterArcTween(begin: begin, end: end); }
hero 的飛行路徑仍然遵循弧形,但圖片的長寬比保持恆定。
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面上次更新於 2024-07-06。 檢視原始碼 或 回報問題。