承接前文👇👇👇
flutter getx 路由侧滑返回 PopScope 回调失效的探究与解决
本次解决方案测试代码基于前文 demo 进行。
运行环境和插件版本:
Flutter (Channel stable, 3.24.5)
Dart SDK version: 3.5.4 (stable)
get: ^5.0.0-release-candidate-9.2.1
go_router: ^14.6.2
添加 PopScope.canPop:false 后,侧滑测试结果:
| 默认效果 | 自定义 PageTransitionsBuilder | |
|---|---|---|
| 基础路由 | ❌ iOS | ✅ iOS |
| go_router | ❌ iOS | ✅ iOS |
| getx | ❌ iOS | ✅ iOS |
注1: 事实上Android端侧滑手势(屏幕下半部分2/3)总是响应是触发系统层面的返回按钮逻辑,与 homebar 上的点击返回按钮是相同的。我们也可以额外添加滑动返回手势(屏幕上半部分1/3)。
注2: 自定义 PageTransitionsBuilder 不仅仅是针对iOS生效,理论来说是支持任意平台的,笔者有测试Android端也可以实现同样的效果。
注3: getx源码有个问题,目前无法获取到传入的theme,导致PageTransitionsBuilder始终为默认平台的builder。可以通过修改代码处理,猜测是大佬也有打盹儿的时候 😂。 👇👇👇 issues 。
正文
一、为什么 PopScope.canPop: false iOS端侧滑会失效
知其然知其所以然,才能够好的去解决问题。
我们依然选择断点调试的方式来查看源码的运行流,来分析手势失效的原因。
查看断点:
在iOS端默认的页面切换效果类中,_CupertinoBackGestureDetector属于手势处理的类。而enabledCallback则是是支持手势滑动。
先看 route.popGestureEnabled的实现:
/// 用户是否可以为这条路由启动弹出手势。
/// 如果用户可以滑动到前一个路由,返回true。
/// 只能在帧之间使用,而不能在构建过程中使用。
@override
bool get popGestureEnabled {
...
//如果尝试取消路由可能会被否决,比如在一个有表单的页面中,那么不要允许用户通过滑动来取消路由。
if (hasScopedWillPopCallback || popDisposition == RoutePopDisposition.doNotPop) {
return false;
}
...
// Looks like a back gesture would be welcome!
return true;
}
可以看到当我们当前路由 hasScopedWillPopCallback == true(过期的API WillPopScope)或者 popDisposition == RoutePopDisposition.doNotPop(这个上一篇分析过了,当route.canPopNotifier==false,即PopScope.canPop设置为false)时,当前页面是不支持手势的侧滑。
现在我们知道了在什么情况下我们页面过渡切换是不生效的。
那我们接着往下看 enabledCallback 在 _CupertinoBackGestureDetector 中是如何控制的手势不生效的(贴关键代码):
红框中的代码,当 enabledCallback==false 手势检测器不会添加记录起点,那么上面一系列的手势监听方法就不会被触发。所以页面切换过渡动画也不会生效,直观来说就是我们页面侧滑没反应。
二、 怎么解决
首先先确定我们针对手势侧滑失效,我们想要实现的目的是什么👇:
PopScope.canPop: false 的情况下,保留iOS端侧滑返回的效果并触发 PopScope.onPopInvokedWithResult 回调
前面分析到 route.popGestureEnabled 为 false时页面会禁用侧滑过渡动画,那么我们是不是通过修改为true就能实现我们的需求的呢?
答案是 肯定的。
但直接修改源码是绝对不可行的,堪称程序员禁忌啊。
那就要实现围魏救赵了。
通过代码栈我们可以找到,如下截图:
Map<TargetPlatform, PageTransitionsBuilder> builders 中iOS默认值是 CupertinoPageTransitionsBuilder()(即我们前半部分分析的源代码),而 builders 是可以通过外部参数传入来解决的,换句话说CupertinoPageTransitionsBuilder 可以自定义为我们想要得到结果,这就确定了围魏救赵的路线。
而 PageTransitionsTheme 是一个主题配置类型,那么在系统主题中,有没有对应的的参数来传入来确定我们自定义的 CupertinoPageTransitionsBuilder 呢,这样子问题就解决了。
部分截图:
OK,现在进行最后一步,完成 CupertinoPageTransitionsBuilder 的自定义。
三、自定义 PageTransitionsBuilder
代码过长,只贴需要有改的部分说说。
1、给路由扩展一个针对 PopScope/WillPopScope 滑动手势的判断(源码修改),无论是否包含拦截组件,依然响应手势滑动的监听。这里也可以直接注释掉,return true 是为了方便查看。
/// extension `ScopeGestureEnabled` for [ModalRoute]
extension _PopGestureEnabledMixinExtension<T> on ModalRoute<T> {
bool get popScopeGestureEnabled {
...
// 修改部分
if (hasScopedWillPopCallback ||
popDisposition == RoutePopDisposition.doNotPop) {
return true;
}
...
// Looks like a back gesture would be welcome!
return true;
}
}
1、增加参数 route ,内部需要获取 route.popDisposition 状态。
static _CupertinoBackGestureController<T> _startPopGesture<T>(
PageRoute<T> route) {
assert(route.popScopeGestureEnabled);
return _CupertinoBackGestureController<T>(
route: route, <------ Add --------
navigator: route.navigator!,
getIsCurrent: () => route.isCurrent,
getIsActive: () => route.isActive,
controller: route.controller!, // protected access
);
}
2、当结束侧滑手势时控制动画效果,如果动画是退出当前页面且路由有拦截时,动画必须执行复原。然后响应PopScope.onPopInvokedWithResult 回调。
// _CupertinoBackGestureController func
void dragEnd(double velocity) {
...
// support for sliding animation
if (!animateForward &&
route.popDisposition == RoutePopDisposition.doNotPop) {
animateForward = true;
route.onPopInvokedWithResult(false, null); // ignore: use result
}
...
}
使用:
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,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
// TargetPlatform.android: PopScopePageTransitionsBuilder(),
TargetPlatform.iOS: PopScopePageTransitionsBuilder(),
},
),
),
routes: {
'/': (context) => const HomePage(),
'/second': (context) => const SecondPage(),
},
);
}
}
贴心的笔者,已经上传到github了,当然插件市场也提交了一份。
结束
源码调试过程比较枯燥,有的时候需要反复尝试才可以找到问题所在。只要能解决就不算浪费时间。
发现问题->确定问题->调试找到病灶->尝试解决问题->确定方案->完成!🎉🎉🎉
测试效果:
👇👇👇 源码地址 pub(pub.dev/packages/po…)