Flutter: iOS 侧滑返回

写在前面

在 Flutter 里,默认是支持 iOS 的屏幕边缘侧滑返回的,但如果由于一些需求,我们对 WillPopScopeonWillPop回调进行了重写,就会导致这个特性失效。

内容

一般情况下,我们没有对页面添加 WillPopScope这个 Widget 并重写它的 onWillPop方法,在 iOS 上是可以在屏幕左侧边缘进行侧滑返回的。但如果我们重写了 onWillPop方法,就会发现这个手势特性失效了。在router.dart文件里,我们可以知道为什么:

// route.dart
  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
    ...
    // If attempts to dismiss this route might be vetoed such as in a page
    // with forms, then do not allow the user to dismiss the route with a swipe.
    if (route.hasScopedWillPopCallback)
      return false;
    ....
    // Looks like a back gesture would be welcome!
    return true;
  }

// routes.dart
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
  ...
    void addScopedWillPopCallback(WillPopCallback callback) {
    assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
    _willPopCallbacks.add(callback);
  }
  
  /// True if one or more [WillPopCallback] callbacks exist.
  ///
  /// This method is used to disable the horizontal swipe pop gesture supported
  /// by [MaterialPageRoute] for [TargetPlatform.iOS] and
  /// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is
  /// disabled.
  ///
  /// The [buildTransitions] method will not be called again if this changes,
  /// since it can change during the build as descendants of the route add or
  /// remove callbacks.
  ///
  /// See also:
  ///
  ///  * [addScopedWillPopCallback], which adds a callback.
  ///  * [removeScopedWillPopCallback], which removes a callback.
  ///  * [willHandlePopInternally], which reports on another reason why
  ///    a pop might be vetoed.
  @protected
  bool get hasScopedWillPopCallback {
    return _willPopCallbacks.isNotEmpty;
  }
 ...
}

// will_pop_scope.dart
class _WillPopScopeState extends State<WillPopScope> {
  ModalRoute<dynamic>? _route;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (widget.onWillPop != null)
      _route?.removeScopedWillPopCallback(widget.onWillPop!);
    _route = ModalRoute.of(context);
    if (widget.onWillPop != null)
      _route?.addScopedWillPopCallback(widget.onWillPop!);
  }

  @override
  void didUpdateWidget(WillPopScope oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(_route == ModalRoute.of(context));
    if (widget.onWillPop != oldWidget.onWillPop && _route != null) {
      if (oldWidget.onWillPop != null)
        _route!.removeScopedWillPopCallback(oldWidget.onWillPop!);
      if (widget.onWillPop != null)
        _route!.addScopedWillPopCallback(widget.onWillPop!);
    }
  }
  }

由于我们重写了 onWillPop,它会把这个回调加入到路由里的_willPopCallbacks列表里,因此这里就禁用了退出的手势操作。

将 onWillPop 置为 null

假如我们确实需要在一些情况下使用到 WillPopScope,那么应该怎么处理呢?我们可以看到把回调加入到队列的前提是 widget.onWillPop != null,所以我们可以结合具体情况,提供一个触发条件,来改变这个onWillPop,类似下面:

  bool condition = true;
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: condition ? () async{
        // do something
        return true; 
        } : null,
      child: Scaffold(
    );
  }

这样我们就可以在较为灵活地处理这个问题了。

Document that WillPopScope prevents swipe to go back on MaterialPageRoute #14203 这个问题里,我们可以看到另外的处理方法。

重写 MaterialPageRoute

class CustomMaterialPageRoute extends MaterialPageRoute {

  @override
  @protected
  bool get hasScopedWillPopCallback {
    return false;
  }
  CustomMaterialPageRoute({
    required WidgetBuilder builder,
    RouteSettings? settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  }) : super(
    builder: builder,
    settings: settings,
    maintainState: maintainState,
    fullscreenDialog: fullscreenDialog,
  );
}

通过重写 MaterialPageRoute,直接将 hasScopedWillPopCallback 改为 false,并在跳转的时候修改实现:

   // old:
   Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return const Second();
      }),
    );
    
    // new:
    Navigator.push(
      context,
      CustomMaterialPageRoute(builder: (context) {
        return const Second();
      }),
    );

但此举会导致的一个问题是,如果你想在返回页面的时候传递数据回去,即使你重写了 onWillPop回调,通过手势返回是不会返回数据的,但 AppBar 上的返回按钮,还是可以将数据返回的。

其它

在处理底部弹窗的返回处理的时候,发现了一个 Android 与 iOS 上的交互不同之处。在 Android 上只要手指在屏幕左侧边缘右滑,就可以收起弹窗。而 iOS 需要你的手指有一个向下的方向,才能收起弹窗。

在 iOS 上,我们总可以感觉到它的手势操作是非常跟手的,如果这个东西是从哪来的,它就应该原路回去。具体可以看[WWDC 2018] Designing Fluid Interfaces 流畅的界面设计

所以我想这可能是 WillPopScope 在 Android 和 iOS 上的表现有所差异的原因。在 Android 上,手指侧滑,就等同于底部返回按钮,很明确的表示要回退。而在 iOS 上,因为它非常的跟手,你虽然在屏幕边缘侧滑,但只要你手指还在屏幕上,你是可以再放回去的。这样在 WillPopScope 的判断意图上,就可能不像 Android 那么清晰。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值