在 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);
}
也就是说:
- 页面的进入和离开动画是成对的,由该页面被推入时使用的 Route 类型决定。
- 全局 pageTransitionsTheme 设置会影响所有使用 MaterialPageRoute 的页面转换。
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
TargetPlatform.values,
value: (dynamic _) => const ZoomPageTransitionsBuilder(),
),
),
),
- 如果没有特别设定 ThemeData 中的 pageTransitionsTheme,在 Android 会使用 ZoomPageTransitionsBuilder、在 iOS 会使用 CupertinoPageTransitionsBuilder。
那意思不就是说,如果我要随时切换 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\',
),
),
);
- MaterialPageRoute 的 iOS 切换动画不如直接使用 CupertinoPageRoute 来的流畅,可以把上面的 CustomMaterialPageRoute 改成 CupertinoPageRoute,但要如何控制 CupertinoPageRoute 的离开动画,这又是另一个问题了⋯⋯
这个解法要注意把初始页面也放在 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 的讨论和程式码对比,可以看出这次改动的主要差异:
- 当一个新路由被推入时,它可以告诉下面的路由如何执行退出动画。
- 下层路由会使用上层路由提供的 delegatedTransition 来替代自己默认的次要转场动画。
- 在 3.27 版本中,主要新增了 delegatedTransition 相关的功能:
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;
}
}
- canTransitionTo 方法的逻辑也有所改变:
// 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 来解决这个问题是最佳解。