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

news2024/10/7 2:20:14

theme: cyanosis

1. 问题的出现

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

|Android 界面 | iOS 界面 | | --- | --- | | 143.gif | 144.gif |

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

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

image.png


2. 关于路由的跳转动画

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

image.png

从中可以看到,不同的平台有不同的 默认 动画变换效果,比如 android 平台使用的是 ZoomPageTransitionsBuilder, ios 和 macos 使用的是 CupertinoPageTransitionsBuilder

image.png

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

image.png

CupertinoPageTransitionsBuilder 中,很容易看到使用的是 SlideTransition ,也就是左右滑动。

image.png

所以 XXXTransitionsBuilder 是可以决定界面路由跳转动画的,而且 PageTransitionsTheme 中提供的是默认值,也就是说我们可以通过主题来指定平台跳转动画的风格。也可以自定义 XXXTransitionsBuilder 实现自己的风格,这块有时间再详细研究整理一下。这里点到为止,以后单写篇文章。现在着重看一下,如何修改之前的路由跳转动画工具,使之可以完成需求。


3. 修正之路的一波三折

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

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

方式一,修改 PageTransitionsTheme 中提供的转换构造器
方式二,继承自 MaterialPageRoute ,并覆写 buildTransitions,自己提供实现。

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

如下,是从右向左跳转动画路由的处理,覆写 buildTransitions 即可控制动画效果,通过覆写 transitionDuration 控制时长。

```dart //右--->左 class Right2LeftRouter extends MaterialPageRoute { 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 animation, Animation secondaryAnimation, Widget child, ) { return SlideTransition( position: Tween ( 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 逻辑:

image.png


继续跟入可以发现,CupertinoPageTransition 之下有一个 _CupertinoBackGestureDetector 的手势检测组件。从名称上很容易看出,它就是处理 iOS 回退的手势事件。从这里不难看出,Flutter 中 iOS 的回退手势,是一种组件行为,而 Android 中的回退返回是一种系统行为。

image.png

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


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

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

首先从类声明上可以看出,它是 StatefulWidget, 也就是说其内部需要维护状态量,重点是其中状态类的构建逻辑,以及状态量的维护逻辑。

image.png


然后,直奔主题,看看其中都构建了些啥。下面是对应状态类的 build 方法,并不是很复杂,通过 Stack 进行叠放,通过 PositionedDirectional 放置一个拖拽区域,使用 Listener 监听手势事件。这里并没有使用 GestureDetector , 可能是因为这里只是水平拖拽事件,使用 Listener 比较直接。

image.png

从上面可以看出,拖拽的区域默认是 20逻辑像素,也就是下面的红框所示。如果你为 MediaQuery 设置了较大的横向 padding,那么宽度可以超越 20 ,但应用很少设置全局的横向 padding 。

dart const double _kBackGestureWidth = 20.0;

image.png


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

这里可以学习一下,源码中通过 Listener 对横向拖拽事件的处理。由于 Listener 组件只能监听到 onPointerDown 事件,也就是触点按下,所以需要额外的东西来追踪这个触点的行为轨迹,这就是 手势检测器。 如果看过 《Flutter 手势探索 - 执掌天下》 小册的朋友,可能比较熟悉。

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

image.png

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

image.png


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

image.png

而处理者就是 _CupertinoBackGestureController,该对象会在开始拖拽时通过 OnStartPopGesture 回调创建,也就是上面看到的 _startPopGesture 静态方法。拖拽更新时,也是该对象通过 dragUpdate 进行的处理。也就是说 _CupertinoBackGestureController 是拥有改变界面形态能力的,比如拖拽更新时,dragUpdate 方法会让界面进行偏移。那它的魔力又从何而来呢?

从源码中可以看出,它持有一个动画控制器,这就很明白了:路由跳转动画本质上就是通过动画控制器来进行变换的。如果是界面是一个木偶,那么动画控制器就是控制木偶的提线,也就是说 _CupertinoBackGestureController 是持有 提线持有者

image.png

最后一点: 提线是如何被 _CupertinoBackGestureController 持有的。其实很容易理解,有些人就是含着金钥匙出生的。

image.png

到这里,所有的破案线索都整理完毕了。现在既想要自定义跳转动画,又想要 iOS 支持回退,可以复制 CupertinoPageTransitionsBuilder 魔改动画变换相关代码。


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

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

image.png

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

image.png


下面来通过一个透明度渐变动画,来做个例子。比如 FlutterUnit 中主页点击搜索框,会透明度渐变跳到搜索页。如果希望 iOS 也是透明度动画,就需要魔改 CupertinoPageTransitionsBuilder 进行处理。比如这里定义一个 FadePageRouter 用于处理透明度渐变路由:

下面是核心代码,主要就是将 CupertinoBackGestureDetector 拿过来,当 iOS 平台是,为 child 套一下。这样 iOS 就可以处理回退的事件,代码详见: fadepageroute.dart。如果想要定义其他的动画,可以在 buildTransitions 中根据 animation 自行处理。

```dart class FadePageRoute extends MaterialPageRoute { 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 animation, Animation secondaryAnimation, Widget child, ) { if (Platform.isIOS) { child = CupertinoBackGestureDetector( enabledCallback: () => isPopGestureEnabled (this), onStartPopGesture: () => startPopGesture (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 回退手势有些坑,下一篇再单独介绍一下,那本文就到这里,谢谢观看 ~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/740418.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多元回归预测 | Matlab基于遗传算法(GA)优化径向基神经网络(GA-RBF)的数据回归预测,多变量输入模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元回归预测 | Matlab基于遗传算法(GA)优化径向基神经网络(GA-RBF)的数据回归预测,多变量输入模型 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %% 清空…

java的BigDecimal操作

1.保留两位小数不进位 public static void main(String[] args) {BigDecimal bigDecimal1 new BigDecimal("1234.8888");String bigDecimal1s bigDecimal1.setScale(2, RoundingMode.DOWN).toPlainString();System.out.println(bigDecimal1s);}结果: …

为什么不建议项目中使用触发器

1.什么是触发器 触发器(trigger)是一种数据库对象,可以看作由事件来触发的特殊存储过程。当一个特定的事件发生时,会自动执行在数据库表上的某些操作,比如当对一个表进行操作(insert,delete&am…

关系型数据库中如何进行事务管理

关系型数据库中如何进行事务管理 在关系型数据库中,事务管理是一项非常重要的功能。它允许数据库管理员在一个或多个数据库操作中实现原子性、一致性、隔离性和持久性(ACID)。 事务是一组数据库操作,它们必须全部执行或全部回滚…

【ACL2023】基于电商多模态概念知识图谱增强的电商场景图文模型FashionKLIP

近日,阿里云机器学习平台PAI与复旦大学肖仰华教授团队、阿里巴巴国际贸易事业部ICBU合作在自然语言处理顶级会议ACL2023上发表基于电商多模态概念知识图谱增强的电商场景图文模型FashionKLIP。FashionKLIP是一种电商知识增强的视觉-语言模型。该论文首先设计了从大规…

String类1

String类 单个字符可以用char类型保存,多个字符组成的文本就需要保存在String对象中,String通常被称为字符串,一个对象最多占用4GB的文本类容。 声明字符串 1.字符串必须包含在“”中 例:”234”、”你好!” 2.声明字…

GOLANG进阶:govalidator过滤器,MD5,JWT身份验证,redis

1.govalidator过滤器:类似于正则匹配,主要放在结构体注释部分,有些验证没有,需要自己替换(把required部分替换成正则表达式) 引入资源包(两种方式): go get github.com/…

ubuntu重启后,docker容器中的mysql怎么重启

1、第一次安装的时候,参考使用docker安装mysql主从集群_docker mysql主从集群_veminhe的博客-CSDN博客 这篇博客操作的。 2、但是突然有一天,我的腾讯云主机被迫重启了,发现docker容器中的mysql挂掉了 3、搜资料看下 参阅了Docker启动mysq…

css之:is()、:where()和:has()伪元素的运用、使用、important

文章目录 简介1、:is()2、:where()3、:has() 简介 :is()、:where()和:has()伪元素是CSS中用于样式化元素的非常强大的工具。它们是在CSS选择器Level4规范中引入的。它们允许我们将样式应用于符合特定条件的任何元素,例如元素的类型、元素的位置和元素的后代。 1、:i…

科研热点|国自然会评季:地方高校申请国家基金难度有多大?

2022年国自然放榜后,一位评审专家的函评意见引发关注。 这位专家在一份函评意的第四部分其他建议中写到: 地方高校的老师不容易。申请人发表了不少好论文,但从未获得过基金支持,应该支持这种在没有项目和条件下还很好开展了研究…

Kubernetes中Pod的升级和回滚

Kubernetes中Pod的升级和回滚 本文说说 Pod 的升级和回滚问题。 当集群中的某个服务需要升级时,我们需要停止目前与该服务相关的所有 Pod,然后下载新版本镜像并创建新的 Pod。如果集群规模比较大,则这个工作变成了一个挑战,而且…

【Linux】 Linus世界,WIndows VS Linux

文章目录 前言WindowsLinux操作系统Windows VS Linux收费情况技术支持安全性开源 区别 前言 在电脑世界有两种十分常见的电脑操作系统——Linux与和Windows,相信对电脑有一定了解的人对它们一定并不陌生!但是在我们的使用过程中,是否有什么事…

大公司为什么禁止在 Spring Boot 项目中使用 @Autowired 注解?

1、说明 最近公司升级框架,由原来的spring framerwork 3.0升级到5.0,然后写代码的时候突然发现idea在属性注入的 Autowired 注解上给出警告提示,就像下面这样的,也挺懵逼的,毕竟这么写也很多年了。 Field injection i…

leetcode 147.对链表进行插入排序

⭐️ 题目描述 🌟 leetcode链接:对链表进行插入排序 思路与图解: 遍历链表,当前结点依次与前面的结点比较,选择插入位置。每次与前面的结点比较需要从头开始比较,所以定义一个 tempHead 指针,…

Python实现PSO粒子群优化算法优化LightGBM回归模型(LGBMRegressor算法)项目实战

说明:这是一个机器学习实战项目(附带数据代码文档视频讲解),如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 PSO是粒子群优化算法(Particle Swarm Optimization)的英文缩写,是一…

CopyRE关系抽取

CopyRE 模型包括编码器和解码器两部分 编码器:将输入的句子(源句子)转换为固定长度的语义向量 解码器:读取该矢量并直接生成三元组 Encoder 编码器使用Bi-RNN对输入句子进行编码。 Decoder 解码器会直接生成三元组。 1、 解码…

仅用js代码实现模态框

很多时候我们经常会用ui框架实现模态框的使用,但是,如果哪一天告诉我们,如何仅用js代码实现一个模态框该怎么办呢? 这里就要用到很多js中的基础方法运用了,我们先看如下代码 var logDiv document.createElement(&quo…

Java版本电子招标采购系统源码:—实现多寻源比价,风险预警

营造全面规范安全的电子招投标环境,促进招投标市场健康可持续发展 传统采购模式面临的挑战 一、立项管理 1、招标立项申请 功能点:招标类项目立项申请入口,用户可以保存为草稿,提交。 2、非招标立项申请 功能点:非招标…

vue2跨组件传值、透明传输($attrs 和 $listeners )

当在 Vue.js 组件中使用 $attrs 和 $listeners 时,它们提供了一种方便的方式来处理传递给组件的属性和事件监听器。 1.$attrs 是一个对象,包含了父组件传递给当前组件但未被当前组件声明的 props。这对于实现“透明传输”非常有用。 2.$listeners 也是…

EMQ 联合英特尔、云轴科技 ZStack 推出泛工业物联网联合解决方案

近日,EMQ 携手英特尔与云轴科技 ZStack 推出泛工业物联网联合解决方案,基于云原生超融合,在挖掘生产数据价值的同时有效降低综合建设成本,为用户提供一站式数据链路及 IT 基础设施解决方案。 工业能耗大户面临的关键挑战 工业正迈入一个全新的物联网时代,海量数据计算需求涌现…