在 Flutter 中,如果當前頁面應用的是 MaterialPageRoute,想要在切換時應用 iOS 風格的切換動畫,最簡單的方法就是切換時直接使用 CupertinoPageRoute。
https://book.flutterchina.club/chapter9/route_transition.html
但是眼尖的我發現了一個奇怪的現象,當使用 MaterialPageRoute 進入頁面 A,然後從頁面 A 使用 CupertinoPageRoute 進入頁面 B 時,頁面 A 的離開動畫仍然使用 MaterialPageRoute 的動畫,而不是預期的 iOS 風格動畫。
我在看很多人的教學文章都沒有提到這點,這讓我非常困惑。
原因分析:
查看 MaterialPageRoute 的原始碼會發現,MaterialPageRoute 的 buildTransitions 方法依賴 Theme.of(context).pageTransitionsTheme。
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
也就是說:
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
TargetPlatform.values,
value: (dynamic _) => const ZoomPageTransitionsBuilder(),
),
),
),
那意思不就是說,如果我要隨時切換 MaterialPageRoute 的進入與離開動畫,自己定義路由動畫不就是不可避免的? 😵
解決方案:
class PageTransitionNotifier extends StateNotifier<PageTransitionsBuilder> {
PageTransitionNotifier() : super(const FadeUpwardsPageTransitionsBuilder());
void setCupertinoStyle() {
state = const CupertinoPageTransitionsBuilder();
}
void setZoomStyle() {
state = const ZoomPageTransitionsBuilder();
}
}
final pageTransitionProvider =
StateNotifierProvider<PageTransitionNotifier, PageTransitionsBuilder>(
(ref) {
return PageTransitionNotifier();
});
class CustomMaterialPageRoute<T> extends MaterialPageRoute<T> {
CustomMaterialPageRoute({
required super.builder,
super.settings,
super.maintainState,
super.fullscreenDialog,
});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return Consumer(
builder: (context, ref, _) {
final pageTransitionsBuilder = ref.watch(pageTransitionProvider);
return pageTransitionsBuilder.buildTransitions<T>(
this,
context,
animation,
secondaryAnimation,
child,
);
},
);
}
}
ref.read(pageTransitionProvider.notifier).setCupertinoStyle();
Navigator.push(
context,
CustomMaterialPageRoute(
builder: (context) => const SecondScreen(
content: 'Cupertino Transition',
),
),
);
這個解法要注意把初始頁面也放在 CustomMaterialPageRoute 中,否則初始頁面還是會受到 MaterialApp 中 ThemeData 的影響。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
initialRoute: '/', // 設置初始路由
onGenerateRoute: (settings) {
if (settings.name == '/') {
return CustomMaterialPageRoute(
builder: (context) => const HomeScreen(),
);
}
return null;
},
);
}
}
以上解法是自己想出來的,歡迎各路大神討論。
然後就在 2024 年 12 月 Flutter 發佈了 3.27 更新, 其中就有提到 Mixing Route Transitions,看到當下我整個傻眼,後來就知道這是一個存在已久的已知問題了,但我想說這個框架都推出這麼久了直到現在才修復是正常的嗎?😂
難怪有人要推出 Flutter 分叉專案 Flock。
回歸正題,從 GitHub PR #150031 的討論和程式碼對比,可以看出這次改動的主要差異:
mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
// 3.27 新增
@override
DelegatedTransitionBuilder? get delegatedTransition => _delegatedTransition;
static Widget? _delegatedTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child) {
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
final TargetPlatform platform = Theme.of(context).platform;
final DelegatedTransitionBuilder? themeDelegatedTransition = theme.delegatedTransition(platform);
return themeDelegatedTransition != null ? themeDelegatedTransition(context, animation, secondaryAnimation, allowSnapshotting, child) : null;
}
}
// 3.24 版本
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
return (nextRoute is MaterialRouteTransitionMixin && !nextRoute.fullscreenDialog)
|| (nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog);
}
// 3.27 版本
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
final bool nextRouteIsNotFullscreen = (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog;
final bool nextRouteHasDelegatedTransition = nextRoute is ModalRoute<T>
&& nextRoute.delegatedTransition != null;
return nextRouteIsNotFullscreen &&
((nextRoute is MaterialRouteTransitionMixin) || nextRouteHasDelegatedTransition);
}
具體內部做了什麼就不深究了,總之把 Flutter 更新到 3.27 來解決這個問題是最佳解。