Flutter 组件集录 | 师于源码 - 与 TapRegion 的相遇

文章主要讨论了如何处理组件外部的点击事件,特别是在浮层显示和隐藏的场景中。作者通过研究微信Windows客户端和有道词典的例子,提出了问题:如何在点击浮层外部时既能隐藏浮层又不阻止事件的正常分发。作者发现Autocomplete组件的实现中使用了TapRegion和FoucusNode,通过监听焦点变化来控制浮层的显示和隐藏。文章详细介绍了TapRegion组件的工作原理,以及如何利用它解决原始问题,无需再依赖全屏Stack来消费事件。最后,作者强调了TapRegion组件的groupId特性,解释了为何点击Autocomplete浮层不会取消焦点的原因。
摘要由CSDN通过智能技术生成

theme: cyanosis

1、缘起

在很久以前,我就对手势中的一种场景耿耿于怀,一度难以解决:

点击 组件之外 的事件如何被响应?

这个功能对于浮层来说是很必要的,如下所示,是微信的 Windows 客户端。点击头像时会弹出一个浮层展示信息,当点击其他位置时,浮层会消失 并且点击的位置可以响应点击事件

77.gif

这就说明浮层可以监听到其外部的点击事件,从而隐藏自己;同时也不会影响到此次的手势事件。这是我之前求而不得的,以前的处理方式是把浮层置于一个全屏的透明 Stack 中,通过监听 Stack 的手势事件触发浮层隐藏。这样的缺点在于: Stack 会消费掉此次事件,导致该事件仅能移除浮层。


另外,外部点击事件对于 焦点 也有使用价值。比如在 有道词典 中,点击其他区域输入框的焦点会被取消,同时隐藏输入框下部的提示面板。另外,对于移动端聊天界面来说,点击输入框外部隐藏键盘也是个常见的需求。

76.gif


下面来说一下我的实际问题,如下所示点击状态按钮弹出状态切换的浮层,此处浮层在全屏的透明 Stack 中,在外部点击 通用设置 时,Stack 消费事件、移除浮层。再点击一下才能激活 通用设置 ,也就是点两次才行,不像微信客户端那样。

78.gif

本文的目的就是探索 组件外部点击事件 的实现方式,来解决这个问题。非常幸运的是,通过对源码的翻阅和追踪,找到了解决方案。下面就一起来看看吧。


2. 从 Autocomplete 组件开始说起

偶然发现,桌面端的 Autocomplete 组件浮层,竟然具有我曾经梦寐以求的 外域点击取消 功能,且不影响此次事件分发。如下所示:当浮层显示时,点击下面的输入框,浮层消失,输入框被激活。

79.gif

这不就是我想要的东西吗!
既然源码中已经实现了,那还等什么!
源码翻烂也要把它的实现方式拎出来!

所以一开始我是从 Autocomplete 组件开始探索的。它是一个 StatelessWidget ,其中的 build 方法依赖于 RawAutocomplete 组件实现。

image.png


RawAutocomplete 继承自 StatefulWidget, 所以浮层的显示和消失逻辑很可能在其状态类中维护。所以直接查阅组件对应状态类的处理逻辑。

image.png

从状态类中可以发现,浮层确实是通过 OverlayEntry 进行实现的。另外浮层定位使用的是 LayerLink ,也就是 《手牵手,一起走 CompositedTransformFollower 与 CompositedTransformTarget》 中介绍的这两个组件。

image.png


所以只要追踪浮层的隐藏事件,就不难查到根源。很明显,浮层显隐是由 _updateOverlay 方法控制的。那么问题来了,当点击外部时是如何触发的呢?

image.png


3. 调试分析隐藏逻辑

想要查看方法触发的时机,最直接的方式就是 debug 调试。 如下所示,是浮层显示时,点击外面区域断点状况。不难发现它是由 FoucusNode 的更新被通知触发的,FoucusNode 本身 ChangeNotifier 的子类。

image.png

可以看出,在状态类初始化时,_foucusNode 会通过 addListener_onChangeFocus 方法作为回调注册。 当 _foucusNode 焦点变化时,就会触发回调,从而实现对浮层移除的功能。

image.png

到这里,可以发现,本质上来说,外界区域的点击影响的是焦点的变化。浮层的移除只是监听了这个事件产生的 副作用 ,而焦点是用于 TextFile 中的,所以下面需要追寻的就是:

对于 TextFiled 而言,外界的点击为什么会让焦点移除。


4. TapRegion 组件的登场

众所周知,TextField 组件是对 EditableText 的一层封装,用于处理输入框边线相关的构建工作。对于 focusNode 并没做实质上的变动,作为构造入参被传入 EditableText 中:

image.png


EditableText 组件及其状态类是个非常复杂的东西,不过我们只以 focusNode 为线索去追觅就会轻松一些。比如下面可以看出 EditableTextState 中定义了 _defaultOnTapOutside , 也就是默认的外界点击事件。其中只有桌面端点击时才会取消焦点,移动端在手指点击时不会取消焦点。这是平台的差异性。这也是为什么 Autocomplete 组件默认在 移动端点击外界无法移除的根本原因。

image.png


到这里,基本上就水落石出了:_defaultOnTapOutside 函数是 TextFieldTapRegion 的默认回调事件。也就是说 TextFieldTapRegion 拥有响应外界点击的能力。

image.png

再进一步看,TextFieldTapRegion 继承自 TapRegion ,其中只不过把 groupId 固定为 EditableText 类型而已,至于 groupId 的作用,等下再说。

class TextFieldTapRegion extends TapRegion { /// Creates a const [TextFieldTapRegion]. /// /// The [child] field is required. const TextFieldTapRegion({ super.key, required super.child, super.enabled, super.onTapOutside, super.onTapInside, super.debugLabel, }) : super(groupId: EditableText); }


5. 使用 TapRegion 组件解决开始的问题

这样,就可以用 TapRegion 组件来试一下,能否解决开始提出的问题。处理方式也很简单,只要为浮层组件套上 TapRegion 监听 onTapOutside ,触发 close 方法关闭浮层即可:

TapRegion( onTapOutside: (_) => close(), child: //

最终完美解决,就不需要使用全屏的 Stack 来消费事件了:

80.gif


6. 介绍一下 groupId 的作用

比如对于 Autocomplete 组件来说,浮层也是输入框的外域,为什么点击浮层没有取消焦点呢?正是因为 TapRegion 中 groupId 的效力,将这个组件视为 一体 ,相当于区域联合起来。所以外界指的是两者区域合集的外界。

image.png

如下所示,Autocomplete 状态类在浮层构建时使用了 TextFieldTapRegion 包裹,也就是说浮层外界组 id 是 EditableText ,这样浮层就会被视为 友军 ,从而达到点击浮层内部,不会触发移除输入框焦点的效果。

image.png


其实总的来看,使用方式很简单,但并不是人人都知道有这个组件。对我来说,探索一个问题,并解决它,是一件很有趣的事。将它分享出来是为了让更多人了解,降低发现它的门槛。毕竟有分析源码的意识和能力还是需要一定功力的,希望本文对你有所帮助。 这玩意真是知道就会,不知道的很难会 ~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值