38

Flutter 在 iOS 下 WillPopScope 导致右滑返回(Swipe Back)失效

 2 years ago
source link: https://sexywp.com/in-ios-willpopscope-disable-swipe-back.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Flutter 在 iOS 下 WillPopScope 导致右滑返回(Swipe Back)失效

在有些场景下,App 为了防止用户误触返回按钮或者误触返回键,导致未保存的结果返回,都会想办法拦截用户的返回行为。WillPopScope 就是做这个用的。这个组件会提供一个回调 onWillPop,当用户尝试返回的时候,会被调用,如果返回 false,则会阻止用户的返回行为。

@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
bottomNavigationBar: BottomNavigationBar(
items: _items,
currentIndex: _currentIndex,
onTap: onTap,
body: IndexedStack(
children: _pageList,
index: _currentIndex,
onWillPop: () {
var now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastClickExitTime > _exitDuration) {
Fluttertoast.showToast(msg: "再按一次退出应用");
_lastClickExitTime = now;
} else {
SystemNavigator.pop(animated: true);
return Future.value(false);
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      child: Scaffold(
        bottomNavigationBar: BottomNavigationBar(
          items: _items,
          currentIndex: _currentIndex,
          onTap: onTap,
        ),
        body: IndexedStack(
          children: _pageList,
          index: _currentIndex,
        ),
      ),
      onWillPop: () {
        var now = DateTime.now().millisecondsSinceEpoch;
        if (now - _lastClickExitTime > _exitDuration) {
          Fluttertoast.showToast(msg: "再按一次退出应用");
          _lastClickExitTime = now;
        } else {
          SystemNavigator.pop(animated: true);
        }
        return Future.value(false);
      },
    );
  }

我一开始没有太注意这个类,和一个安卓的同事搭档的时候,看到他引入了这个组件,才想明白了,很多安卓手机上,有实体或者模拟的返回键,很容易误触,所以确实很需要这个回调。上面是一个使用 onWillPop 防止误触返回的例子。

不过,随着研发的深入,我发现一个很严重的问题。在 iOS 上我有一个习惯动作,就是从屏幕的左边沿,向右滑动,就可以返回上一个屏幕,在 iOS 上,这个动作有个专有名词,叫 Swipe Back,右滑返回。如果我用一个 WillPopScope 套住 Scaffold 的话,iOS 上这个页面,Swipe Back 就会完全失效。手指滑动上去,完全没有反应。而且令人发指的是,onWillPop 也完全不会被触发。等于说,在 iOS 上,这个类的作用仅有一个,就是 Swipe Back 手势被取消,回调也永远不会触发。

这真是太不美了,经过搜索,我发现这个是官方一个很著名的 issue #14203,2018 年 1 月打开,无数人反馈,至今没有一个明确的回应。有人(可能是官方)表示,这本来就是一个期望中的行为,只是文档没有说明。

也有观点认为,“只是拦截这个动作,而且 onWillPop 也可以返回 true,还是允许返回的,为什么在 iOS 就彻底失灵了呢?” —— 我也是这么想的。不过,我细细一想,也有点能理解实现者的做法,在 iOS 上,这个动作会有个配套的动画,右滑的时候,会好像翻书一样的视觉效果。如果这时候 App 不允许用户返回的话,滑到一半,这个动画的物理表现到底是怎么样的呢?像皮筋一样弹回?还是怎么做呢?确实有点恼火。还不如完全就没响应。

不过,这就太苦了我们这种想要在 iOS 上保证交互效果的开发。有某个老哥做了一个 CupertinoWillPopScope 插件,试图要去解决这个问题,不过我看了一眼,很不喜欢他的实现。他的实现其实基于一个事实,就是 WillPopScope 里,如果 onWillPopnull 的话,在 iOS 上,就不会阻止 Swipe Back,这个我自己写个变量也可以控制,何至于引入一个插件,一个变量就能解决的事情。

我自己遇到的一个场景,也确实很恼火,在 App 里,我引用了 flutter_inappwebview 这个插件,主要是提供了一个查看网页的 WebView,在 WebView 里,我想实现的效果是,Swipe Back 的时候,如果网页有 history,只是返回上一页,如果没有 history,就退出 WebView。这就需要我每次都能拦截 Swipe Back 这个动作,然后判定后决定到底是在网页里后退,还是干脆退出 WebView。

不过,因为刚才说的 WillPopScope 的问题,导致了很奇怪的局面。当我用了以后。在 iOS 上,效果诡异,我永远失去了 Swipe Back 退出 WebView 的能力,但是,在网页上后退却不受影响。只是退到第一页的时候,就再也滑不动了,不能滑动退出 WebView。如果我去掉 WillPopScope,则不论你在哪个页面,都会导致滑动直接退出 WebView,无法实现返回上一页的功能。

只剩下一个办法,就是通过判断 canGoBack,来决定是否绑定 onWillPop,就可以完全实现我的想法,不过 canGoBack 又是一个 Future 类型,我又不知道怎么写了。唉。问题仍然还是没有解决的,我特别记录在此,给遇到同样问题的同学一些参考,同时也希望高手看到了不吝赐教。

@override
Widget build(BuildContext context) {
mUrl = ModalRoute.of(context)?.settings.arguments.toString() ?? "";
final double pixel = MediaQuery.of(context).size.width / 375;
return WillPopScope(
child: Scaffold(
appBar: MyAppBar.create(
title: _pageTitle ?? "",
pixel: pixel,
actions: [
IconButton(
onPressed: () {
NavigatorUtils.goBack(context);
icon: Icon(Icons.close),
body: _webPageContent(),
onWillPop: !_canGoBack
? null
: () /** 此逻辑在 iOS 下无效 */ {
var canGoBack = webViewController?.canGoBack() ?? Future.value(false);
canGoBack.then((value) {
if (value) {
webViewController?.goBack();
} else {
Navigator.pop(context);
return Future.value(false);
  @override
  Widget build(BuildContext context) {
    mUrl = ModalRoute.of(context)?.settings.arguments.toString() ?? "";
    final double pixel = MediaQuery.of(context).size.width / 375;
    return WillPopScope(
      child: Scaffold(
        appBar: MyAppBar.create(
          title: _pageTitle ?? "",
          pixel: pixel,
          actions: [
            IconButton(
              onPressed: () {
                NavigatorUtils.goBack(context);
              },
              icon: Icon(Icons.close),
            ),
          ],
        ),
        body: _webPageContent(),
      ),
      onWillPop: !_canGoBack
          ? null
          : () /** 此逻辑在 iOS 下无效 */ {
              var canGoBack = webViewController?.canGoBack() ?? Future.value(false);
              canGoBack.then((value) {
                if (value) {
                  webViewController?.goBack();
                } else {
                  Navigator.pop(context);
                }
              });
              return Future.value(false);
            },
    );
  }

上述代码里,_canGoBack 这个变量,是我自己模拟的,但是这个变量的效果和 flutter_inappwebview 提供的就不太一样了,插件提供的 API,返回一个 Future<bool> 类型,在任何时候都可以判定当前网页是否存在 history 栈,但是我自己实现的 _canGoBack 变量,只是判定该网页已经不是首次加载的那个,经历过跳转。在我的场景里,大多数网页,只是看一眼就返回了,用户不会循着链接一层层深入,所以在很大程度上可以达到我要的效果,但是,显然没有真正解决这个问题,如果用户浏览了两个页面,就会退回我上文说的效果了。只是略微好了一点点。

— End —


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK