如何在使用CupertinoPageRoute的Flutter MaterialApp中检测iOS返回滑动手势
你现有使用CupertinoPageRoute的路由配置本身是默认开启iOS边缘侧滑返回能力的,只是组件本身没有直接暴露手势对应的事件回调,可以根据你的实际业务场景选择对应的实现方案:
通用场景:拦截所有路由退出操作(含侧滑、返回按钮、代码pop)
如果不需要严格区分退出操作的触发来源,只要在路由即将退出、或者已经完成退出时执行业务逻辑,直接使用Flutter官方提供的PopScope组件包裹业务页面即可,不管是侧滑手势、导航返回按钮、代码调用Navigator.pop,都会触发对应的回调。
示例代码(对应你业务页面的实现):
class ProductScreen extends StatelessWidget { static const id = 'product_screen'; @override Widget build(BuildContext context) { return PopScope( // 控制是否允许当前页面pop canPop: true, // pop动作触发后的回调,didPop为true表示路由已经成功退出 onPopInvoked: (bool didPop) { if (didPop) { // 在这里执行页面退出时的业务逻辑 debugPrint('页面已退出'); } }, child: Scaffold( // 原有页面内容 ), ); } }
如果使用的是Flutter 3.16以下的低版本,没有PopScope组件,可以替换为已兼容的WillPopScope组件,逻辑一致。
精准场景:单独识别iOS系统侧滑返回手势
如果需要严格区分“用户手动侧滑触发的返回”和“其他方式触发的返回”,甚至需要拿到侧滑过程中的实时拖动进度、松手后的回弹/完成状态,可以通过监听路由本身的动画状态实现,不需要改动原有路由结构,也不会和系统手势产生冲突:
实现原理:用户手动侧滑拖动返回时,路由的动画值是被手势实时控制的,此时路由的isAnimating属性为false;当用户松手后,系统会启动自动过渡动画完成剩下的进度,此时isAnimating属性会变为true。通过这个状态差,就可以精准识别侧滑手势的触发时机。
方式1:在单个业务页面内实现检测
在需要检测的页面的State中添加路由监听即可:
class _ProductScreenState extends State<ProductScreen> { ModalRoute? _currentRoute; @override void didChangeDependencies() { super.didChangeDependencies(); _currentRoute = ModalRoute.of(context); _currentRoute?.animation?.addListener(_onRouteAnimationChange); } void _onRouteAnimationChange() { if (_currentRoute == null) return; // 手动拖动阶段(用户正在侧滑) if (!_currentRoute!.isAnimating) { final dragProgress = _currentRoute!.animation!.value; debugPrint('侧滑拖动中,当前进度:$dragProgress'); // 在这里可以加对应的业务处理,比如暂停视频播放、收起弹窗等 } // 松手阶段(系统开始播放回弹/完成pop的动画) else { debugPrint('用户结束侧滑拖动'); // 如果最终animation值回到1,说明用户中途松手、页面回弹,没有退出 // 如果最终animation值降到0,说明用户拖到边界、页面成功pop } } @override void dispose() { _currentRoute?.animation?.removeListener(_onRouteAnimationChange); super.dispose(); } }
方式2:封装通用路由组件,全局复用检测逻辑
如果需要在多个路由中统一加侧滑检测能力,可以自定义继承CupertinoPageRoute的封装类,把监听逻辑统一写在路由基类里,不需要每个页面重复写代码:
class ObservableCupertinoPageRoute<T> extends CupertinoPageRoute<T> { // 透传的手势回调 final void Function(double dragProgress)? onUserSwipeDragging; final VoidCallback? onUserSwipeRelease; ObservableCupertinoPageRoute({ required super.builder, super.settings, this.onUserSwipeDragging, this.onUserSwipeRelease, }); @override void install() { super.install(); // 统一挂载动画监听 animation?.addListener(() { if (!isAnimating) { onUserSwipeDragging?.call(animation!.value); } else { onUserSwipeRelease?.call(); } }); } }
之后你只需要把原有路由配置里的CupertinoPageRoute替换成这个自定义类,就可以在创建路由时传入对应的回调,原有逻辑完全不需要改动:
onGenerateRoute: (RouteSettings settings) { switch (settings.name) { case ProductScreen.id: return ObservableCupertinoPageRoute( builder: (_) => ProductScreen(), settings: settings, onUserSwipeDragging: (progress) { debugPrint('商品页侧滑拖动进度:$progress'); }, onUserSwipeRelease: () { debugPrint('商品页用户结束侧滑'); }, ); // 其他路由配置同理替换即可 } }
注意事项
- 不要自行在页面外层加监听左边缘拖动的
GestureDetector,这类自定义手势会和系统自带的侧滑返回手势在手势竞技场产生竞争,很容易导致原生的侧滑返回效果失效,体验生硬。 - 当你通过
PopScope设置canPop: false禁止页面pop时,系统的侧滑手势会自动被禁用,这是CupertinoPageRoute的默认行为,不需要额外处理。 - 上述检测逻辑完全复用系统原生的侧滑手势实现,不会破坏原有的平滑过渡动画效果。
内容的提问来源于stack exchange,提问作者Kinuhatek

