應用程式畫面的轉場看起來絲滑可以增加使用者體驗,反過來說轉場不流暢也會影響體驗。
如同我們之前見過的, MaterialPageRoute
和 CupertinoPageRoute
可以在 Navigator
中加入路由。在我們的範例中你可能已經注意到在新舊路由交換的過程中會加入轉場效果。一般會使用平台預設的效果,當然我們也可以自訂。
例如在 Android 中切換頁面時由下向上淡入,離開頁面時則採用相反的效果。在 iOS 系統,頁面從右邊滑入,離開頁面則相反。除了預設的效果,Flutter 也支援通過加入頁面之間的轉場自訂效果。為了完成這個功能,我們需要深入解析路由。
PageRouteBuilder
是一個輔助類別可以用來自訂建立 Route
,取代使用內建的路由如 MaterialPageRoute
和 CupertinoPageRoute
。
PageRouteBuilder
包含了許多 callback 和屬性可以協助定義 PageRoute
。下面是一些關鍵的參數:
transitionBuilder
這是我們指定轉場動畫的地方,具體來說這也是一個 Builder
函式須回傳組件。pageBuilder
這是指定轉場後新畫面的地方。也是一個 Builder
函式,回傳組件。transitionDuration
轉場的持續時間barrierColor
和 barrierDismissible
設定局部覆蓋路由。舉例來說點擊按鈕後要彈出對話框,但對話框後面依然是原本畫面且可以看到,這種情況下 barrierColor
設定對話框後面的背景色,barrierDismissible
設定點擊外部區域是否可以關閉視窗。簡單的說 transitionBuilder
設定轉場動畫,持續時間由 transitionDuration
設定。接著顯示新畫面 pageBuilder
,如果是 Modal 效果則搭配 barrierColor
和 barrierDismissible
等。
使用 PageRouteBuilder
搭配這些參數可以建立自訂的轉場效果。
在開始之前讓我們先回到 Navigator 1.0 的範例:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Navigator 1.0 範例',
home: MyHomePage(title: '首頁'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
bool? outcome = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const DestinationDetails(destination: '項目 1'),
),
);
if (outcome == true) {
scaffoldMessenger
.showSnackBar(const SnackBar(content: Text("項目 1 加入最愛")));
}
},
child: const Text('項目 1'),
),
ElevatedButton(
onPressed: () async {
bool? outcome = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const DestinationDetails(destination: '項目 2'),
),
);
if (outcome == true) {
scaffoldMessenger
.showSnackBar(const SnackBar(content: Text("項目 2 加入最愛")));
}
},
child: const Text('項目 2'),
),
],
),
),
);
}
}
class DestinationDetails extends StatelessWidget {
const DestinationDetails({super.key, required this.destination});
final String destination;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(destination)),
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(destination),
ElevatedButton(
child: const Text('加入最愛'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
ElevatedButton(
child: const Text("返回"),
onPressed: () {
Navigator.of(context).pop(false);
},
),
]),
),
);
}
}
首先修改 Navigator
的子組件 ElevatedButton
,現在我們使用 PageRouteBuilder
取代 MaterialPageRoute
。
原本的程式如下:
ElevatedButton(
onPressed: () async {
bool? outcome = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const DestinationDetails(destination: '項目 1'),
),
);
if (outcome == true) {
scaffoldMessenger
.showSnackBar(const SnackBar(content: Text("項目 1 加入最愛")));
}
},
child: const Text('項目 1'),
),
在 onPressed
參數我們代入匿名函式並使用 MaterialPageRoute
。接著在 MaterialPageRoute
中代入下一個畫面的組件,上面範例是 DestinationDetails
組件。
現在讓我們換成使用 PageRouteBuilder
ElevatedButton(
child: const Text('項目 1'),
onPressed: () async {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return DestinationDetails(title: "項目 1");
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return child;
}
)
);
}
),
比起單純使用 MaterialPageRoute
,現在使用 PageRouteBuilder
,可以看到現在在 pageBuilder
參數回傳我們的 DestinationDetails
組件。
若現在執行程式碼則你會發現沒有任何轉場效果,這是因為我們在 transitionBuilder
沒有做任何操作。假如我們希望加入滑入的效果可以如下調整:
transitionBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(animation),
child: child
);
}
上面範例我們不再單純的回傳 child
組件,我們使用了 SlideTransition
組件封裝動畫邏輯;轉場效果由左而右直到完整呈現畫面。其子組件 DestinationDetails
置放於 SlideTransition
裡,因此新畫面可以覆蓋之前的頁面。接著當我們返回上頁時動畫則是運行相反的效果。
這個範例中有個小複雜的地方就是 Tween
和 Offset
類別我們還未詳細探討關於動畫的部分,後續章節將深入說明。
現在,如果你預計在每個頁面使用相同的效果,那麼可以繼承 PageRouteBuilder
類別來建立可重用的轉場,後續你就可以像使用 MaterialPageRoute
或 CupertinoPageRoute
一樣。如此可以避免重複程式碼也可以輕鬆的調整轉場效果。
例如,如果你希望整個應用程式都使用 SlideTransition
的效果。那麼你可以如下自訂 MyRoute
類別:
class MyRoute extends PageRouteBuilder {
final Widget target;
MyRoute({ required this.target }): super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) => target,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) => SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(animation),
child: child
),
);
}
在這個例子,我們使用單一的參數 target
就是我們要轉場的組件。作為建構子的一部分,類別設定了父類別 PageRouteBuilder
的參數,pageBuilder
為匿名函式並接受對應參數,最後單純的回傳 target
。而 transitionsBuilder
也一樣傳入一些參數,並回傳我們之前使用過的 SildeTransition
。
乍看之下好像很複雜,但其實如果認真解析會發現,其實只是在建構子的地方傳遞兩個匿名函式的參數到父類別。
因此當 MyRoute
物件被建立時,會自動呼叫 PageRouteBuilder
的建構子並代入兩個參數。也就是包了一層的 PageRouteBuilder
而已。
現在在 ElevatedButton
中,我們可以如之前一樣在 onPressed
函式使用:
ElevatedButton(
child: Text("點擊"),
onPressed: () {
Navigator.of(context).push(
MyRoute(
transitionPage: DestinationDetails(
title: "項目"
),
),
);
},
)
我們只需要加入自訂的路由組件到導航堆疊,其就會自動具備 SlideTransition
的效果。
內建還有很多不同的轉場效果可以使用
ScaleTransition
逐漸放大效果RotationTransition
旋轉效果FadeTransition
淡入只需要簡單的替換類別就可以取得需要的效果。
現在我們已經學習了如何切換畫面,接下來我們將進一步探索如何傳遞狀態。
幾乎所有的應用程式都有狀態的概念。這裡的狀態比起單一組件中的狀態會包含更多東西,因為使用者在使用整個應用程式的流程都需要存取。如過你曾經使用過其他框架,你可能見識過各種不同處理狀態的方式。
在 Flutter 中並沒有唯一處理共享狀態的方式。
在後續介紹套件的段落,我們會介紹持久性的儲存方式例如:雲端資料庫。不過當我們從資料庫讀取資料到狀態中又該如何在各個頁面中使用?
值得一提的是,關於狀態管理並沒有絕對的正確方式,任何一種方式都有它們的優缺點,而我們需要從可維護性、程式碼的可讀性,和應用程式情境的角度來決定使用何種方式。
在應用程式中共享狀態最簡單的方式,也是大部分開發者在 Flutter 中管理狀態的方式就是通過建構子參數的方式在頁面之間傳遞狀態。在 DestinationDetails
組件我們已經看到了這種用法,通過 title
傳遞使用者查看的狀態。另一個常見的情境,假設有一個使用者登入應用程式,此時我們會建立關於使用者資料的物件以存取相關資訊。當使用者瀏覽其他頁面的時候我們可以將這個物件傳遞到任意組件。這個方式的優點是非常簡單,但缺點也非常明顯:
但在現階段我們依舊會使用這種方式,來保持程式清晰易懂,後續當你學習更多 Flutter 和 Dart 的使用,你可以進一步使用一些進階的狀態管理。
到此我們探索了應用程式中畫面的概念,了解如何切換。首先是使用 Navigator
組件。然後搭配使用 Route
使用了具名路由以及轉換的過程如何傳遞參數。接著了解了如何自訂轉場效果,上面最後我們提及了關於「狀態」的事情,後續我們將繼續介紹狀態管理。