在使用Compose开发过程中,经常会遇到涉及事件分发的场景。例如布局嵌套,以及两个Compose组件重叠。
下面分开来讲:
-
布局嵌套事件分发
下面看看在Compose实现一次点击事件监听的写法:
Modifier.pointerInput(Unit) { awaitPointerEventScope { awaitPointerEvent(PointerEventPass.Initial) Log.d("TAG", "点击") } }awaitPointerEventScope是一个手势事件的作用域,awaitPointerEvent是一个挂起函数,这里传入了一个参数:PointerEventPass,而这个参数是控制事件分发顺序的关键。PointerEventPass是一个枚举类,主要有三个值,Initial, Main, Final。在Compose嵌套布局中,每个组件都设置awaitPointerEvent并且传入PointerEventPass就可以控制分发的顺序了。
分发顺序:
PointerEventPass.Initial -> PointerEventPass.Main -> PointerEventPass.Final
下面写段代码验证一下,
@Composable fun BoxDemo() { Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() .background(Color.Yellow) .pointerInput(Unit) { awaitPointerEventScope { awaitPointerEvent(PointerEventPass.Initial) Log.d("TAG", "第一层") } } ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .size(400.dp) .background(Color.Blue) .pointerInput(Unit) { awaitPointerEventScope { awaitPointerEvent(PointerEventPass.Final) Log.d("TAG", "第二层") } } ) { Box( Modifier .size(200.dp) .background(Color.Green) .pointerInput(Unit) { awaitPointerEventScope { awaitPointerEvent(PointerEventPass.Main) Log.d("TAG", "第三层") } } ) } } }可以看到有3个Box进行了嵌套,点击之后输出:
14:20:08.411 D 第一层 14:20:08.412 D 第三层 14:20:08.412 D 第二层这个顺序刚好是对应了:
PointerEventPass.Initial -> PointerEventPass.Main -> PointerEventPass.Final
另外说明一下,使用上面的代码只能监听一次点击,因为awaitPointerEventScope只响应一次点击事件,如果需要监听每一次的点击事件,只需要把awaitPointerEventScope替换成awaitEachGesture就可以了。
-
组件重叠
如果两个组件出现了重叠,并且只需要响应其中一个组件的点击事件,只需要设置其中一个组件的Modifier.clickable即可监听。又或者,2个组件不完全重叠,只需要响应点击到的组件就行,这个时候两个组件设置Modifier.clickable也都是可以的,只要点击到就能监听到。现在出现了另外几个场景,A组件在底部,B组件在上面,A、B组件都设置了Modifier.clickable,布局方式有:A组件和B组件完全重叠;A组件小于B组件并且被B组件完全覆盖了;B组件覆盖了A组件部分位置。需求是在这几种布局方式中,某些条件下点击B组件的位置需要响应A组件的点击事件。一般情况下,两个组件有重叠区域,并且都设置了Modifier.clickable,那么在上面的组件,会把点击事件消费,从而底部的组件不会有任何事件响应。这个时候需要用到Modifier的一个拓展函数pointerInteropFilter,拦截过滤事件,达到事件分发的作用。
pointerInteropFilter函数源码如下:
@ExperimentalComposeUiApi fun Modifier.pointerInteropFilter( requestDisallowInterceptTouchEvent: (RequestDisallowInterceptTouchEvent)? = null, onTouchEvent: (MotionEvent) -> Boolean ): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "pointerInteropFilter" properties["requestDisallowInterceptTouchEvent"] = requestDisallowInterceptTouchEvent properties["onTouchEvent"] = onTouchEvent } ) { val filter = remember { PointerInteropFilter() } filter.onTouchEvent = onTouchEvent filter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent filter }可以看到是一个实验的函数,需要在组件外部添加:@OptIn(ExperimentalComposeUiApi::class)
如下:
@OptIn(ExperimentalComposeUiApi::class) @Composable fun BoxDemo2() { }参数解释:
onTouchEvent:
具有类似于返回类型的 View.onTouchEvent布尔返回类型。如果提供的 onTouchEvent 返回 true,它将继续接收事件流(除非事件流已被截获),如果返回 false,则不会。
requestDisallowInterceptTouchEvent
是一个 lambda,您可以选择提供它,以便以后可以调用它(是的,在这种情况下,您调用您提供的 lambda),这类似于调用 ViewParent.requestDisallowInterceptTouchEvent。调用此值时,树中遵守合约的任何关联祖先都会采取相应的行动,不会拦截偶数流
这里只介绍onTouchEvent参数,这个onTouchEvent和View系的事件分发的onTouchEvent是一样的,返回true,表示消费了事件,不会继续往下分发,false表示继续传递。
如果两个组件都设置了pointerInteropFilter,并且onTouchEvent都返回true,那么这2个组件不会有任何的事件监听响应。代码如下:
@OptIn(ExperimentalComposeUiApi::class) @Composable fun BoxDemo2() { Box { Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() .background(Color.Yellow) .pointerInteropFilter { true } .clickable { Log.d("TAG", "A组件") } ) { } Box( contentAlignment = Alignment.Center, modifier = Modifier .size(400.dp) .background(Color.Blue) .pointerInteropFilter { true } .clickable { Log.d("TAG", "B组件") } ) { } } }那么可以知道,如果组件中设置了pointerInteropFilter,onTouchEvent返回true将会表示本次事件已经被消费了,设置的clickable不会被响应,也不会继续分发点击事件。
在A组件铺满屏幕,B组件覆盖了A组件400.dp大小的位置的布局中,A、B组件都设置clickable,某些条件下,希望点击B组件那一块的位置,A组件会得到事件响应,B组件不响应,那么B组件可以设置Modifier.pointerInteropFilter { false },如果有其它的条件判断可以返回不同值。
@OptIn(ExperimentalComposeUiApi::class) @Composable fun BoxDemo2() { Box { Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() .background(Color.Yellow) .clickable { Log.d("TAG", "A组件") } ) { } Box( contentAlignment = Alignment.Center, modifier = Modifier .size(400.dp) .background(Color.Blue) .pointerInteropFilter { false } .clickable { Log.d("TAG", "B组件") } ) { } } }输出:
15:18:05.889 D A组件
点击了B组件,事件没有被消费继续分发到A组件响应。