GoRouter 页面转场动画完全指南:用 CustomTransitionPage 为每条 GoRoute 定制过渡效果
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
GoRouter 是 Flutter 官方团队维护的声明式路由库,本指南基于其官方文档 transition-animations.md,讲解如何为每个GoRoute定制专属的转场动画。通过本文,你将掌握pageBuilder与CustomTransitionPage的完整用法、transitionsBuilder的动画原理、全部可配置参数的含义,并看到可运行的仓库示例与测试验证,从而在项目里实现淡入淡出、缩放、滑动、对话框遮罩等任意自定义转场效果。
一、为什么需要自定义转场动画
默认情况下,GoRouter 会按照平台惯例为页面切换使用内置动画:在 Material 应用中生成MaterialPage,在 Cupertino 应用中生成CupertinoPage(对应实现见 pages/material.dart 与 pages/cupertino.dart)。这些默认转场开箱即用,但存在两个局限:
- 全局统一:默认转场对所有路由一视同仁,无法为某个特殊页面(如详情页、模态框、引导页)单独设计入场效果;
- 不可定制:动画曲线、时长、遮罩行为均无法按路由调整。
CustomTransitionPage正是为打破这两个局限而生。从源码看,它的定位非常明确(见 custom_transition_page.dart):
"Page with custom transition functionality. To be used instead of MaterialPage or CupertinoPage, which provide their own transitions."
也就是说,只要你在路由的pageBuilder中返回CustomTransitionPage,该页面就会完全绕开平台的默认转场,转由你提供的transitionsBuilder决定进出场动画。
二、核心概念:pageBuilder 与 CustomTransitionPage
1. pageBuilder 是定制转场的入口
GoRoute提供两种页面构建方式:
builder:返回一个普通的Widget,GoRouter 内部会将其包装成平台默认的MaterialPage/CupertinoPage;pageBuilder:返回一个Page<Object?>对象,你可以完全掌控页面类型,因此它是自定义转场的唯一入口。
两者的取舍在 builder.dart 中有明确体现:_buildPageForGoRoute会优先使用pageBuilder,只有当它不存在时才回退到builder并用平台适配器包装。
从 route.dart 还可以看到 GoRoute 的构造函数断言:builder、pageBuilder、redirect三者至少提供一个,且若提供了onExit,则必须同时有builder或pageBuilder。这意味着「自定义转场」和「离开拦截回调」可以组合使用。
2. CustomTransitionPage 的官方示例
原文档给出了最基础、也最具代表性的淡入淡出(Fade)转场示例,这是定制转场的「标准骨架」:
GoRoute( path: 'details', pageBuilder: (context, state) { return CustomTransitionPage( key: state.pageKey, child: DetailsScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { // 基于动画值,使用曲线改变页面的透明度 return FadeTransition( opacity: CurveTween(curve: Curves.easeInOutCirc).animate(animation), child: child, ); }, ); }, ),这段代码有三个关键点需要理解:
key: state.pageKey:每个路由状态都有唯一的pageKey,必须把它传给CustomTransitionPage,保证页面在路由重建、深链跳转时状态一致;transitionsBuilder的四个参数:context(构建上下文)、animation(本路由的主动画,push 时从 0→1,pop 时从 1→0)、secondaryAnimation(被压在下方的路由动画,用于实现"上层切换时下层同步缩放/平移"等联动效果)、child(页面本体,必须原样返回或包裹);- 曲线动画组合:
CurveTween(curve: ...).animate(animation)是 Flutter 中给动画套用缓动曲线的标准写法,这里选用了Curves.easeInOutCirc让淡入淡出带有"先慢后快再慢"的韵律感。
3. 仓库示例:三种转场形态一网打尽
原文档提到的完整示例位于仓库的 example/lib/transition_animations.dart。它在/根路由下嵌套了三个子路由,分别演示了三种典型的自定义转场场景,非常值得逐段研读。
场景一:标准淡入淡出(duration 150ms)
GoRoute( path: 'details', pageBuilder: (BuildContext context, GoRouterState state) { return CustomTransitionPage<void>( key: state.pageKey, child: const DetailsScreen(), transitionDuration: const Duration(milliseconds: 150), transitionsBuilder: ( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return FadeTransition( opacity: CurveTween(curve: Curves.easeInOut).animate(animation), child: child, ); }, ); }, ),与文档版相比,示例补充了transitionDuration(入场时长 150ms),并把曲线换成了更通用的Curves.easeInOut。
场景二:可点击遮罩关闭的对话框页面
GoRoute( path: 'dismissible-details', pageBuilder: (BuildContext context, GoRouterState state) { return CustomTransitionPage<void>( key: state.pageKey, child: const DismissibleDetails(), barrierDismissible: true, // 点击遮罩可关闭 barrierColor: Colors.black38, // 半透明黑色遮罩 opaque: false, // 不遮挡下层路由 transitionDuration: Duration.zero, transitionsBuilder: (_, _, _, Widget child) => child, // 无转场,瞬开 ); }, ),这一场景展示了用CustomTransitionPage模拟模态对话框的完整套路:barrierDismissible: true让用户点击页面外的遮罩即可返回,opaque: false表示该页面不透明地覆盖下层——从 custom_transition_page.dart 的注释可知,不透明路由在入场完成后,下层路由将不再被构建以节省资源,而透明路由则始终保留下层渲染,这正是对话框场景所需要的。transitionDuration: Duration.zero配合恒等transitionsBuilder实现"瞬间弹出"。
场景三:正反转场时长不对称
GoRoute( path: 'custom-reverse-transition-duration', pageBuilder: (BuildContext context, GoRouterState state) { return CustomTransitionPage<void>( key: state.pageKey, child: const DetailsScreen(), barrierDismissible: true, barrierColor: Colors.black38, opaque: false, transitionDuration: const Duration(milliseconds: 500), // 入场 500ms reverseTransitionDuration: const Duration(milliseconds: 200), // 退场 200ms transitionsBuilder: ( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return FadeTransition(opacity: animation, child: child); }, ); }, ),入场慢、退场快的"快进快出"效果,可以让模态页的关闭响应更跟手。
整个示例通过context.go('/details')等命令式 API 驱动跳转(见 transition_animations.dart),导航按钮都定义在HomeScreen中,读者可直接运行go_router/example目录下的示例应用逐一点击体验。
三、transitionsBuilder 参数与动画机制详解
transitionsBuilder的类型签名是:
Widget Function( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, )它在 custom_transition_page.dart 中被定义。源码注释揭示了它的调用时机与语义,理解这些对写出正确动画至关重要:
- 调用时机:每当路由在可见状态下状态发生变化(例如当前路由的
canPop值改变)时,transitionsBuilder都会被重新调用; animation的驱动方向:当 Navigator 把新路由压入栈顶时,主动画从0.0运行到1.0(入场);当用户按返回键弹出栈顶路由时,主动画从1.0运行到0.0(退场)。因此,动画是否"反转"完全由导航方向决定,你在 builder 里只需面向animation的值编写映射即可;secondaryAnimation的作用:它代表栈中下一层路由的动画,常被用来实现"上层页面切换时,下层页面轻微缩放/位移"的沉浸式效果;child的职责:child是路由的真实内容(由buildPage返回),它被 Semantics 包裹以保证无障碍语义(scopesRoute: true),你的transitionsBuilder必须原样返回它,或用各种过渡组件把它包起来。
从 builder.dart 的调用链可以确认:pageBuilder返回的Page对象最终会交给_CustomTransitionPageRoute(继承自 Flutter 的PageRoute),transitionDuration、reverseTransitionDuration、barrierDismissible、barrierColor、barrierLabel、maintainState、fullscreenDialog、opaque等参数会被逐一透传到PageRoute的对应 getter(见 custom_transition_page.dart),从而真正影响导航行为——这保证了你在配置里写的每个参数都不是摆设。
四、CustomTransitionPage 全部可配置参数
以下参数全部来自 CustomTransitionPage 构造函数,结合源码注释整理其含义与默认值:
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
child | Widget(必填) | — | 路由展示的内容,即你的页面 Widget |
transitionsBuilder | 函数(必填) | — | 定义页面进场/退场动画的构建函数 |
transitionDuration | Duration | 300ms | 入场动画时长 |
reverseTransitionDuration | Duration | 300ms | 退场(pop)动画时长 |
maintainState | bool | true | 路由进入非激活状态时是否保留内存中的 widget 树;若设为false,框架会完全丢弃不可见路由的子树以省资源(但需注意:被压住的路由持有的 Future 在下一路由弹出时可能无法正常 resolve) |
fullscreenDialog | bool | false | 是否为全屏对话框;Material/Cupertino 中会令 AppBar 显示关闭按钮而非返回按钮,iOS 上对话框转场方式不同且不能用返回滑动手势关闭 |
opaque | bool | true | 转场完成后该路由是否遮住下层路由;true时下层路由停止构建以省资源 |
barrierDismissible | bool | false | 是否可通过点击模态遮罩关闭本路由 |
barrierColor | Color? | null(透明) | 模态遮罩颜色 |
barrierLabel | String? | null | 遮罩的无障碍语义标签,当遮罩可点击时,VoiceOver 等读屏工具聚焦遮罩会朗读该文本 |
key/name/arguments/restorationId | — | — | 继承自 FlutterPage的基础属性,其中key务必传入state.pageKey |
实际项目中"对话框 + 遮罩 + 自定义动画"是最常见的组合,官方测试 custom_transition_page_test.dart 演示了完整用法:
GoRoute( path: '/dismissible-modal', pageBuilder: (_, GoRouterState state) => CustomTransitionPage<void>( key: state.pageKey, barrierDismissible: true, transitionsBuilder: (_, _, _, Widget child) => child, child: const DismissibleModal(key: dismissibleModalKey), ), ),测试通过router.push('/dismissible-modal')打开页面,再tester.tapAt(const Offset(50, 50))点击左上角遮罩区域,验证页面被成功关闭——这正是barrierDismissible生效的直接证据。
五、进阶:常用转场效果模板
理解了transitionsBuilder的机制后,可以自由组合 Flutter 内置的动画组件实现各类效果。以下模板可直接套用(以本仓库示例为基底扩展):
滑动进入(Slide)
transitionsBuilder: (context, animation, secondaryAnimation, child) { final offset = Tween<Offset>( begin: const Offset(1, 0), // 从右侧滑入 end: Offset.zero, ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)); return SlideTransition(position: offset, child: child); },缩放进入(Scale)
transitionsBuilder: (context, animation, secondaryAnimation, child) { return ScaleTransition( scale: Tween<double>(begin: 0.8, end: 1.0).animate(animation), child: FadeTransition(opacity: animation, child: child), ); },双层联动(利用 secondaryAnimation)
transitionsBuilder: (context, animation, secondaryAnimation, child) { // 下层页面随上层入场轻微缩小,形成视觉层级感 return ScaleTransition( scale: Tween<double>(begin: 1.0, end: 0.95).animate(secondaryAnimation), child: child, ); },完全禁用转场(瞬切):GoRouter 内置了NoTransitionPage,它在 custom_transition_page.dart 中定义为transitionDuration与reverseTransitionDuration均为Duration.zero、transitionsBuilder恒等返回child的CustomTransitionPage子类,适合引导页、登录页等不希望有动画干扰的场景:
GoRoute( path: 'login', pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const LoginScreen()), ),官方测试 custom_transition_page_test.dart 专门验证了NoTransitionPage在 push 与 pop 两个方向都不产生任何过渡动画。
六、测试验证:转场动画的可测性
GoRouter 的转场配置有完整的测试支撑,这保证了自定义转场在生产环境中可被自动化验证:
- 示例级冒烟测试:example/test/transition_animations_test.dart 启动示例 App,依次点击"Go to the Details screen"→"Go back to the Home screen",用
pumpAndSettle()等待动画结束并断言页面正确切换——验证了CustomTransitionPage示例的端到端可用性; - 单元级行为测试:test/custom_transition_page_test.dart 覆盖四种行为——
transitionsBuilder被正确调用并构建 child、NoTransitionPage双向无动画、点击遮罩关闭路由、正反转场时长不同(通过对比 push 与 pop 的pumpAndSettle()次数来断言transitionDuration与reverseTransitionDuration确实生效)。
如果你在自己的项目中引入CustomTransitionPage,可以仿照上述测试编写testWidgets用例,使用pumpAndSettle()推进动画帧,用find.byType(FadeTransition)等 finder 断言转场组件是否出现在 widget 树中。
七、总结
GoRouter 的自定义转场能力可以概括为一条主线:GoRoute.pageBuilder是开关,CustomTransitionPage是载体,transitionsBuilder是动画灵魂。官方文档 transition-animations.md 给出的淡入淡出示例虽短,却完整包含了pageKey、曲线动画组合、四参数 builder 签名等全部核心要素;而仓库中的 transition_animations.dart 示例与 custom_transition_page.dart 源码则进一步揭示了参数默认值、底层PageRoute透传机制与NoTransitionPage等进阶用法。掌握这套机制后,你可以在不引入任何第三方动画库的前提下,为应用中的每个页面打造独一无二、贴合产品气质的转场体验。
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考