1、缘起
在很久以前,我就对手势中的一种场景耿耿于怀,一度难以解决:
点击
组件之外
的事件如何被响应?
这个功能对于浮层来说是很必要的,如下所示,是微信的 Windows 客户端。点击头像时会弹出一个浮层展示信息,当点击其他位置时,浮层会消失 并且点击的位置可以响应点击事件 。
这就说明浮层可以监听到其外部的点击事件,从而隐藏自己;同时也不会影响到此次的手势事件。这是我之前求而不得的,以前的处理方式是把浮层置于一个全屏的透明 Stack
中,通过监听 Stack
的手势事件触发浮层隐藏。这样的缺点在于: Stack 会消费掉此次事件,导致该事件仅能移除浮层。
另外,外部点击事件对于 焦点
也有使用价值。比如在 有道词典
中,点击其他区域输入框的焦点会被取消,同时隐藏输入框下部的提示面板。另外,对于移动端聊天界面来说,点击输入框外部隐藏键盘也是个常见的需求。
下面来说一下我的实际问题,如下所示点击状态按钮弹出状态切换的浮层,此处浮层在全屏的透明 Stack
中,在外部点击 通用设置
时,Stack
消费事件、移除浮层。再点击一下才能激活 通用设置
,也就是点两次才行,不像微信客户端那样。
本文的目的就是探索 组件外部点击事件
的实现方式,来解决这个问题。非常幸运的是,通过对源码的翻阅和追踪,找到了解决方案。下面就一起来看看吧。
偶然发现,桌面端的 Autocomplete 组件浮层,竟然具有我曾经梦寐以求的 外域点击取消
功能,且不影响此次事件分发。如下所示:当浮层显示时,点击下面的输入框,浮层消失,输入框被激活。
这不就是我想要的东西吗! 既然源码中已经实现了,那还等什么! 源码翻烂也要把它的实现方式拎出来!
所以一开始我是从 Autocomplete 组件开始探索的。它是一个 StatelessWidget
,其中的 build
方法依赖于 RawAutocomplete
组件实现。
RawAutocomplete 继承自 StatefulWidget
, 所以浮层的显示和消失逻辑很可能在其状态类中维护。所以直接查阅组件对应状态类的处理逻辑。
从状态类中可以发现,浮层确实是通过 OverlayEntry
进行实现的。另外浮层定位使用的是 LayerLink
,也就是 《手牵手,一起走 CompositedTransformFollower 与 CompositedTransformTarget》 中介绍的这两个组件。
所以只要追踪浮层的隐藏事件,就不难查到根源。很明显,浮层显隐是由 _updateOverlay
方法控制的。那么问题来了,当点击外部时是如何触发的呢?
想要查看方法触发的时机,最直接的方式就是 debug 调试。 如下所示,是浮层显示时,点击外面区域断点状况。不难发现它是由 FoucusNode
的更新被通知触发的,FoucusNode 本身 ChangeNotifier
的子类。
可以看出,在状态类初始化时,_foucusNode
会通过 addListener
将 _onChangeFocus
方法作为回调注册。 当 _foucusNode
焦点变化时,就会触发回调,从而实现对浮层移除的功能。
到这里,可以发现,本质上来说,外界区域的点击影响的是焦点的变化。浮层的移除只是监听了这个事件产生的 副作用
,而焦点是用于 TextFile 中的,所以下面需要追寻的就是:
对于 TextFiled 而言,外界的点击为什么会让焦点移除。
众所周知,TextField 组件是对 EditableText 的一层封装,用于处理输入框边线相关的构建工作。对于 focusNode
并没做实质上的变动,作为构造入参被传入 EditableText 中:
EditableText 组件及其状态类是个非常复杂的东西,不过我们只以 focusNode
为线索去追觅就会轻松一些。比如下面可以看出 EditableTextState
中定义了 _defaultOnTapOutside
, 也就是默认的外界点击事件。其中只有桌面端点击时才会取消焦点,移动端在手指点击时不会取消焦点。这是平台的差异性。这也是为什么 Autocomplete 组件默认在 移动端点击外界无法移除的根本原因。
到这里,基本上就水落石出了:_defaultOnTapOutside
函数是 TextFieldTapRegion
的默认回调事件。也就是说 TextFieldTapRegion
拥有响应外界点击的能力。
再进一步看,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);
}
这样,就可以用 TapRegion 组件来试一下,能否解决开始提出的问题。处理方式也很简单,只要为浮层组件套上 TapRegion
监听 onTapOutside
,触发 close
方法关闭浮层即可:
TapRegion(
onTapOutside: (_) => close(),
child: //
最终完美解决,就不需要使用全屏的 Stack 来消费事件了:
比如对于 Autocomplete 组件来说,浮层也是输入框的外域,为什么点击浮层没有取消焦点呢?正是因为 TapRegion
中 groupId 的效力,将这个组件视为 一体
,相当于区域联合起来。所以外界指的是两者区域合集的外界。
如下所示,Autocomplete 状态类在浮层构建时使用了 TextFieldTapRegion
包裹,也就是说浮层外界组 id 是 EditableText
,这样浮层就会被视为 友军 ,从而达到点击浮层内部,不会触发移除输入框焦点的效果。
其实总的来看,使用方式很简单,但并不是人人都知道有这个组件。对我来说,探索一个问题,并解决它,是一件很有趣的事。将它分享出来是为了让更多人了解,降低发现它的门槛。毕竟有分析源码的意识和能力还是需要一定功力的,希望本文对你有所帮助。 这玩意真是知道就会,不知道的很难会 ~