1. 问题的呈现

因为之前一直在 Android 机子上测试,没在 iOS 上跑过。最近 FlutterUnit 发布了 iOS 版本,收到了最多的反应便是:回来滑动 失效。 起先我以为只是 WillPopScope 的锅,但我发现,许多一般的界面在跳转后,iOS 也无法回来滑动。然后觉得很蹊跷,事出失常必有妖,且来一探究竟。

Android 界面 iOS 界面
FlutterUnit 周边 | 深入分析 iOS 手势回退问题
FlutterUnit 周边 | 深入分析 iOS 手势回退问题

在上图 iOS 界面中,点击 关于蜜蜂 进入界面能够正常滑动回来,但跳转到 账号材料 就无法滑动回来了。所以,很自然地想到查看相关的源码位置,看看处理的差异性。然后看到,关于蜜蜂 是经过 MaterialPageRoute 跳转的,账号材料 经过 Right2LeftRouter 跳转的。然后查看了其他几个不能回退的界面,能够实锤:

我之前封装的界面跳转动画的辅佐路由类有问题!!!

FlutterUnit 周边 | 深入分析 iOS 手势回退问题


2. 关于路由的跳转动画

Right2LeftRouter 是跳转界面时,能够从左向右跳转动画的辅佐器。这是在 4 年前前写的,相关文章是 : Flutter福利篇 | Hero转场组件共享 — 附赠-路由动画东西类 ,没想到今天被挖祖坟了。咱们都知道 MaterialPageRoute 跳转的作用,在 Android 中是 透明度 + 缩放动画。这点能够在源码中看到,如下是构建跳转动画的逻辑:其间根据上下文获取 PageTransitionsTheme 来执行:

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

从中能够看到,不同的渠道有不同的 默许 动画改换作用,比方 android 渠道运用的是 ZoomPageTransitionsBuilder, ios 和 macos 运用的是 CupertinoPageTransitionsBuilder

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

再跟进看一下:ZoomPageTransitionsBuilder 在进入时运用了 _ZoomEnterTransition 组件,其间定义了透明度和缩放的动画。这便是 Android 渠道下会 透明度 + 缩放 动画跳转界面的根源。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

CupertinoPageTransitionsBuilder 中,很简略看到运用的是 SlideTransition ,也便是左右滑动。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

所以 XXXTransitionsBuilder 是能够决定界面路由跳转动画的,而且 PageTransitionsTheme 中供给的是默许值,也便是说咱们能够经过主题来指定渠道跳转动画的风格。也能够自定义 XXXTransitionsBuilder 完结自己的风格,这块有时间再具体研究收拾一下。这儿点到为止,以后单写篇文章。现在侧重看一下,怎么修正之前的路由跳转动画东西,使之能够完结需求。


3. 批改之路的好事多磨

一开始的想法(偏): 其实也很简略,之前承继的是 PageRouteBuilder无法完结需求,而它和 MaterialPageRoute 是兄弟,都是 PageRoute 的派生类。所以改成承继 MaterialPageRoute 就行了,最大的问题便是怎么修正 MaterialPageRoute 的跳转动画作用。

从上面咱们知道,决定跳转动画最中心的是 buildTransitions 办法,它是定义在 MaterialRouteTransitionMixin 中的,被 MaterialPageRoute 混入。所以能改动动画处理办法的手法有两个:

办法一,修正 PageTransitionsTheme 中供给的转化结构器
办法二,承继自 MaterialPageRoute ,并覆写 buildTransitions,自己供给完结。

从耦合的角度来看,办法一 要优秀一些,能够自定义 XXXTransitionsBuilder 来随意插拔,并且能操控各个渠道的作用。这儿因为之前代码里是承继体系,为了不破坏已有代码,所以这儿还选用办法二。这种办法的好处在于,你能够拜访和操控更多的细节,比方动画的时长。各有利弊,完结起来也都是对动画器的操作,本质上并没有什么区别。

如下,是从右向左跳转动画路由的处理,覆写 buildTransitions 即可操控动画作用,经过覆写 transitionDuration 操控时长。

//右--->左
class Right2LeftRouter<T> extends MaterialPageRoute<T> {
  final Widget child;
  final Duration duration;
  final Curve curve;
  Right2LeftRouter({
    required this.child,
    this.duration = const Duration(milliseconds: 300),
    this.curve = Curves.fastOutSlowIn,
  }) : super(builder: (_) => child);
  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(1.0, 0.0),
        end: const Offset(0.0, 0.0),
      ).animate(CurvedAnimation(parent: animation, curve: curve)),
      child: child,
    );
  }
  @override
  Duration get transitionDuration => duration;
}

但是成果事与愿违,iOS 运用上面的 Right2LeftRouter 滑动回退也不可,此处与 MaterialPageRoute 的唯一区别便是自定义了 buildTransitions 。 所以现在所有的线索,都将问题指向了一点: CupertinoPageTransitionsBuilder,它内部必定对回退手势做出了什么响应。

这么看来,想自定义 iOS 的跳转转化动画,就比较费事了。回退手势是在 CupertinoPageTransitionsBuilder 中处理的,所以官方的弦外之音是:乖乖用我的,别乱搞。

但我并不是什么乖小孩,iOS 默许的动画是进入页自右向左进入,但假如想完结透明度突变进入等其他动画,而且支撑手势回退,就比较费事。不入虎穴焉得虎子,去探探路吧。


4. 深入虎穴,分析 CupertinoPageTransitionsBuilder 的行为

如下所示,CupertinoPageTransitionsBuilder 运用了 CupertinoRouteTransitionMixin.buildPageTransitions 静态办法处理的 buildTransitions 逻辑:

FlutterUnit 周边 | 深入分析 iOS 手势回退问题


继续跟入能够发现,CupertinoPageTransition 之下有一个 _CupertinoBackGestureDetector 的手势检测组件。从称号上很简略看出,它便是处理 iOS 回退的手势事情。从这儿不难看出,Flutter 中 iOS 的回退手势,是一种组件行为,而 Android 中的回退回来是一种系统行为。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

这也是为什么上面承继自 MaterialPageRoute ,无法完结回退作用的原因。你没有种下一颗种子,自然不会有什么东西发芽。


下面来看一下 _CupertinoBackGestureDetector 回退检测组件的具体处理:其间有两个回调函数,两者逻辑都是由静态办法所完结:

enabledCallback: 回来 bool 值的函数,用于表示是否能够回退。
onStartPopGesture :回来 _CupertinoBackGestureController 的函数,开始回退手势触发时。

首先从类声明上能够看出,它是 StatefulWidget, 也便是说其内部需求维护状况量,重点是其间状况类的构建逻辑,以及状况量的维护逻辑。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题


然后,直奔主题,看看其间都构建了些啥。下面是对应状况类的 build 办法,并不是很复杂,经过 Stack 进行叠放,经过 PositionedDirectional 放置一个拖拽区域,运用 Listener 监听手势事情。这儿并没有运用 GestureDetector , 或许是因为这儿只是水平拖拽事情,运用 Listener 比较直接。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

从上面能够看出,拖拽的区域默许是 20逻辑像素,也便是下面的红框所示。假如你为 MediaQuery 设置了较大的横向 padding,那么宽度能够超越 20 ,但使用很少设置大局的横向 padding 。

const double _kBackGestureWidth = 20.0;

FlutterUnit 周边 | 深入分析 iOS 手势回退问题


5. 跟源码学习手势事情的处理

这儿能够学习一下,源码中经过 Listener 对横向拖拽事情的处理。因为 Listener 组件只能监听到 onPointerDown 事情,也便是触点按下,所以需求额定的东西来追踪这个触点的行为轨迹,这便是 手势检测器。 假如看过 《Flutter 手势探索 – 执掌天下》 小册的朋友,或许比较熟悉。

如下所示,在状况类中维护了 HorizontalDragGestureRecognizer 水平拖拽手势检测器,手势检测器在初始化状况时被创立、也需求在 dispose 时被毁掉,这便是组件为什么是 StatefulWidget 的原因。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

水平拖拽手势检测器创立完后,接下来需求将检测器和触点进行关联。这个事情非常显着,便是 Listener 组件监听到触点按下时,如下所示。 其实仔细想想,这和 GestureDetector 组件的逻辑处理并无二致。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题


到这儿,手势事情的逻辑就很清楚了,HorizontalDragGestureRecognizer 检测触点,并在对应的时机触发相关回调,比方开始拖拽时,和拖拽更新等。检测器所供给的的是事情类型已经携带的数据,至于界面需求根据事情和数据做出什么反应,需求外界在回调中自行处理。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

而处理者便是 _CupertinoBackGestureController,该目标会在开始拖拽时经过 OnStartPopGesture 回调创立,也便是上面看到的 _startPopGesture 静态办法。拖拽更新时,也是该目标经过 dragUpdate 进行的处理。也便是说 _CupertinoBackGestureController 是具有改动界面形状才能的,比方拖拽更新时,dragUpdate 办法会让界面进行偏移。那它的魔力又从何而来呢?

从源码中能够看出,它持有一个动画操控器,这就很理解了:路由跳转动画本质上便是经过动画操控器来进行改换的。假如是界面是一个木偶,那么动画操控器便是操控木偶的提线,也便是说 _CupertinoBackGestureController 是持有 提线持有者

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

最终一点: 提线是怎么被 _CupertinoBackGestureController 持有的。其实很简略理解,有些人便是含着金钥匙出世的。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

到这儿,所有的破案线索都收拾结束了。现在既想要自定义跳转动画,又想要 iOS 支撑回退,能够复制 CupertinoPageTransitionsBuilder 魔改动画改换相关代码。


6. 想自定义动画路由动画需求注意什么

其实翻看源码之后,知道 CupertinoPageTransition 本质上也是基于 SlideTransition,而且进行了几个动画曲线的处理,说实话作用挺不错。 然后手里之前写的,像褴褛一样的动画玩意忽然不香了,如下所示,之前我自己写的便是个简略的 SlideTransition

FlutterUnit 周边 | 深入分析 iOS 手势回退问题

一般情况下,有 Flutter 的动画作用就基本够用了,要想一下是否真有必要去做些更花里胡哨的跳转动画。下面是Flutter 内置了四种跳转动画,但只有 _CupertinoBackGestureDetector 处理了 iOS 回退手势的校验。所以想要自定义动画作用,就必须魔改 CupertinoPageTransitionsBuilder 完结。

FlutterUnit 周边 | 深入分析 iOS 手势回退问题


下面来经过一个透明度突变动画,来做个例子。比方 FlutterUnit 中主页点击查找框,会透明度突变跳到查找页。假如期望 iOS 也是透明度动画,就需求魔改 CupertinoPageTransitionsBuilder 进行处理。比方这儿定义一个 FadePageRouter 用于处理透明度突变路由:

下面是中心代码,首要便是将 CupertinoBackGestureDetector 拿过来,当 iOS 渠道是,为 child 套一下。这样 iOS 就能够处理回退的事情,代码详见: fade_page_route.dart。假如想要定义其他的动画,能够在 buildTransitions 中根据 animation 自行处理。

class FadePageRoute<T> extends MaterialPageRoute<T> {
  final Widget child;
  final Duration duration;
  final Curve? curve;
  FadePageRoute({
    required this.child,
    this.duration = const Duration(milliseconds: 300),
    this.curve,
  }) : super(builder: (_) => child);
  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    if (Platform.isIOS) {
      child = CupertinoBackGestureDetector(
        enabledCallback: () => isPopGestureEnabled<T>(this),
        onStartPopGesture: () => startPopGesture<T>(this),
        child: child,
      );
    }
    if (curve != null) {
      animation = CurvedAnimation(
        parent: animation,
        curve: curve!,
      );
    }
    return FadeTransition(
      opacity: Tween(begin: 0.1, end: 1.0).animate(animation),
      child: child,
    );
  }
  @override
  Duration get transitionDuration => duration;
}

这篇洋洋洒洒快写一万字了,经过本次的探索,我对路由动画有了许多新的知道。也给出了一个较好的自定义路由动画办法。期望我们也能从源码的处理中学到一些常识,而不是单单为了解决问题只拿答案,而是培育自己解决问题的才能。WillPopScope 我看了一下源码,对 iOS 回退手势有些坑,下一篇再独自介绍一下,那本文就到这儿,谢谢观看 ~