在上一篇 《Compose 事件分发(上) 寻找触摸点》中已经介绍,在触摸 compose 组件时,会从根节点开始遍历,获取命中的 PointerInputFilter,然后对其进行事件分发,今天,我们来重点讲解一下事件的分发过程,并且在 AndroidView 上,嵌套原生 View 的时候,事件的分发过程
一、示例
AppTheme { // Box 组件 Box(modifier = Modifier .background(Color.Gray) .pointerInput(Unit) { detectTapGestures(onPress = { Log.i("TAG", "detectTapGestures 100 onPress") }) }.size(300.dp) ){ // Row 组件 Row( modifier = Modifier .background(Color.Yellow) .pointerInput(Unit) { detectTapGestures(onPress = { Log.i("TAG", "detectTapGestures 50 onPress") }) }.size(150.dp) ){} } } 复制代码
这次我们的示例更改一下,添加两个带有 pointInput 的组件 Box 和 Row,以便更好的查看事件响应。
二、分析
1、Compose 组件事件分发分析
继续回到 pointerInputEventProcessor.process 方法:
@OptIn(InternalCoreApi::class) // 1、root 为 AndroidComposeView 传进来的根节点 internal class PointerInputEventProcessor(val root: LayoutNode) { ... fun process( pointerEvent: PointerInputEvent, positionCalculator: PositionCalculator ): ProcessResult { // 收集 PointerInputFilter 集 // 6、分发事件 Dispatch to PointerInputFilters val dispatchedToSomething = hitPathTracker.dispatchChanges(internalPointerEvent) .... return ProcessResult(dispatchedToSomething, anyMovementConsumed) } 复制代码
我们来查看下 dispatchChanges 方法:
fun dispatchChanges(internalPointerEvent: InternalPointerEvent): Boolean { // 1、遍历子节点,分发 main 事件 var dispatchHit = root.dispatchMainEventPass( internalPointerEvent.changes, rootCoordinates, internalPointerEvent ) // 2、遍历子节点,分发 final 事件 dispatchHit = root.dispatchFinalEventPass() || dispatchHit return dispatchHit } 复制代码
这里的 root 再介绍一下,引用上文:
将 hitResult 集合设置到 hitPathTracker 中,内部会对 hitResult 集合转成 Node 链表,在分发时会遍历该链表,需要注意的是,这个链表的顺序是从 parent layoutNode 到 child LayoutNode 的顺序,跟 view 分发一致
- 遍历子节点,本质就是遍历 pointInput,分发 main 事件
- 遍历子节点,本质就是遍历 pointInput,分发 final 事件
来看下 dispatchMainEventPass 的处理:
override fun dispatchMainEventPass( changes: Map<PointerId, PointerInputChange>, parentCoordinates: LayoutCoordinates, internalPointerEvent: InternalPointerEvent ): Boolean { // Build the cache that will be used for both the main and final pass buildCache(changes, parentCoordinates, internalPointerEvent) return dispatchIfNeeded { val event = pointerEvent!! val size = coordinates!!.size // 1、分发 Initial 事件 pointerInputFilter.onPointerEvent(event, PointerEventPass.Initial, size) // Dispatch to children. if (pointerInputFilter.isAttached) { // 2、继续遍历子节点递归分发 children.forEach {it.dispatchMainEventPass(relevantChanges,coordinates!!, internalPointerEvent)} } if (pointerInputFilter.isAttached) { // 3、分发 Main 事件 pointerInputFilter.onPointerEvent(event, PointerEventPass.Main, size) } } } 复制代码
Compose 对一个事件分了三种类型,目的是更好的处理事件,翻译自注释:
- Initial :允许祖先在后代之前使用 PointerInputChange 的各个方面。例如,滚动条可能会阻止按钮在滚动开始后被其他手指点击
- Main :手势过滤器应该对 PointerInputChanges 的各个方面做出反应和使用的主要通道。这是后代将在父母之前与 PointerInputChanges 交互的主要路径。这允许按钮在底部的容器响应点击之前响应点击。
- Final :在这个过程中,后代可以了解在 Main 过程中祖先使用了 PointerInputChanges 的哪些方面。例如,这是一个按钮如何确定它不应再响应手指
离开它的方式,因为父滚动条已经消耗了 PointerInputChange 中的移动。
为了不陷入源码调用陷阱,这里结合示例用图表示调用过程:
Main 会对事件进行消费处理,这也是为什么子组件优先消费事件的原因,也即示例 demo 中,如果我们点击 Row 区域的话,响应的是 Row,而不是 Box。
事件的消费处理,是调用 pointInput 设置的 pointerInputFilter 的 onPointerEvent 方法,我们需要回到示例 demo,找到 pointInput,进入源码探索:
fun Modifier.pointerInput( key1: Any?, block: suspend PointerInputScope.() -> Unit ): Modifier = composed( ... ) { val density = LocalDensity.current val viewConfiguration = LocalViewConfiguration.current // 1、pointerInputFilter 的实现类是 SuspendingPointerInputFilter remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply { LaunchedEffect(this, key1) { // 2、启用挂起函数,block 为示例 demo 中的 detectTapGestures block() } } } 复制代码
这里我们需要关注两个点:
- pointerInputFilter 的实现类是 SuspendingPointerInputFilter,我们需要进入到该类查看 onPointerEvent 的调用
- 利用 LaunchedEffect,从可组合项内安全调用挂起函数,block 为示例中设置的 detectTapGestures 挂起函数,需要注意的是,block 是在 apply 于 SuspendingPointerInputFilter 作用域内的,后面的扩展函数会调用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法
detectTapGestures 可以理解成是订阅者,SuspendingPointerInputFilter 为事件的发布者,在 SuspendingPointerInputFilter 收到事件调用 onPointerEvent 方法时,会触发该订阅者,订阅者处理事件是否消费,并且还可以处理是单击、双击还是长按,然后回调自己的各个函数。
我们先来看下事件的发布者 SuspendingPointerInputFilter 的 onPointerEvent:
internal class SuspendingPointerInputFilter( override val viewConfiguration: ViewConfiguration, density: Density = Density(1f) ) : PointerInputFilter(),PointerInputModifier,PointerInputScope,Density by density { private val pointerHandlers = mutableVectorOf<PointerEventHandlerCoroutine<*>>() ... // 1、发布者会调用该方法来创建一个协程,并添加到 pointerHandlers 集合中 override suspend fun <R> awaitPointerEventScope( block: suspend AwaitPointerEventScope.() -> R ): R = suspendCancellableCoroutine { continuation -> val handlerCoroutine = PointerEventHandlerCoroutine(continuation) synchronized(pointerHandlers) { pointerHandlers += handlerCoroutine block.createCoroutine(handlerCoroutine, handlerCoroutine).resume(Unit) } continuation.invokeOnCancellation { handlerCoroutine.cancel(it) } } .... override fun onPointerEvent(pointerEvent: PointerEvent,pass: PointerEventPass,bounds: IntSize ) { ... dispatchPointerEvent(pointerEvent, pass) ... } // 2、遍历 pointerHandlers ,触发 offerPointerEvent 方法 private fun dispatchPointerEvent( pointerEvent: PointerEvent,pass: PointerEventPass) { forEachCurrentPointerHandler(pass) { it.offerPointerEvent(pointerEvent, pass) } } ... } private inner class PointerEventHandlerCoroutine<R>(private val completion: Continuation<R>,) : AwaitPointerEventScope, Density by this@SuspendingPointerInputFilter, Continuation<R> { private var awaitPass: PointerEventPass = PointerEventPass.Main ... fun offerPointerEvent(event: PointerEvent, pass: PointerEventPass) { // 2、判断事件类型是否是 Main 事件 if (pass == awaitPass) { // 3、判断 pointerAwaiter 是否为空 pointerAwaiter?.run { pointerAwaiter = null resume(event) } } } ... override suspend fun awaitPointerEvent( pass: PointerEventPass ): PointerEvent = suspendCancellableCoroutine { continuation -> awaitPass = pass // 4、pointerAwaiter 的赋值 pointerAwaiter = continuation } } 复制代码
- 发布者会调用该方法来创建一个协程,并添加到 pointerHandlers 集合中
- 遍历 pointerHandlers 的 offerPointerEvent 方法发布事件
- 判断事件类型是否是 Main 事件
- 判断 pointerAwaiter 是否为空,如果不为空的话,则恢复挂起函数
- 挂起函数的注册,对 pointerAwaiter 进行赋值
然后我们再跟进 detectTapGestures,看下订阅者的处理:
suspend fun PointerInputScope.detectTapGestures( onDoubleTap: ((Offset) -> Unit)? = null, onLongPress: ((Offset) -> Unit)? = null, onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture, onTap: ((Offset) -> Unit)? = null ) = coroutineScope { ... val channel = Channel<TapGestureEvent>(capacity = Channel.UNLIMITED) ... launch{ // 1、事件转换后最终结果,通过 channel 来阻塞等待结果的返回,例如会回调 onDoubleTap、onLongPress 等 } // 2、遍历手势 forEachGesture { // 3、调用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法,创建并注册个协程 awaitPointerEventScope { // 4、处理最终事件消费的地方,然后将事件处理的最终结果发送至 channel translatePointerEventsToChannel( this@coroutineScope,channel,consumeOnlyDownsSignal, consumeAllUntilUpSignal) } } 复制代码
- 事件转换后最终结果,通过 channel 来阻塞等待结果的返回,例如会回调 onDoubleTap、onLongPress 等
- 遍历手势,内部会执行 block,并且会挂起等待所有的 Final 事件结束
- 调用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法,创建启动并注册个协程
- 处理最终事件消费的地方,然后将事件处理的最终结果发送至 channel
现在我们也大致理解了整体过程,这里还是通过绘制图来总结,避免代码太多干扰思路:
由于内容篇幅太长,这里只对 down 事件进行讲解,进入
translatePointerEventsToChannel:
private suspend fun AwaitPointerEventScope.translatePointerEventsToChannel( scope: CoroutineScope, channel: SendChannel<TapGestureEvent>, detectDownsOnly: State<Boolean>, consumeAllUntilUp: MutableState<Boolean> ) { ... // 1、判断事件有无消费,如果没有消费的话则进入 else if (event.changes.fastAll { it.changedToDown() }) { // 2、从 event 中取出事件 val change = event.changes[0] // 3、消费 down 事件,其实就是设置 consumed.downChange = true change.consumeDownChange() // 4、将 down 结果通过 channel 发送出去 channel.trySend(Down(change.position, change.uptimeMillis)) } ... } 复制代码
- 判断事件有无消费,如果没有消费的话则进入
- 从 PointerEvent 中取出事件
- 消费 down 事件,其实就是设置 consumed.downChange = true
- 将 down 结果通过 channel 发送出去
消费 down 事件时标记 downChage 为 true 很重要,因为我们的 pointerInputFilter 有 2 个,并且在处理 Main 事件时,是从子组件往父组件开始遍历,也即子组件会先消费事件,在消费了事件之后,遍历到父组件时,则进入不了这个判断,也就不处理。
2、AndroidView 组件事件分发分析
通过上面的分析知道,Compose 组件是通过 SuspendingPointerInputFilter 实现事件的处理,那 AndroidView 组件是怎么分发的呢?继续贴一下之前的图:
我们可以直接看下 AndroidViewHolder,在返回的 layoutNode 中,有预设一个 pointerFilter:
val layoutNode: LayoutNode = run { // Prepare layout node that proxies measure and layout passes to the View. val layoutNode = LayoutNode() val coreModifier = Modifier .pointerInteropFilter(this) .... layoutNode.modifier = modifier.then(coreModifier) 复制代码
进入 pointerInteropFilter 查看代码:
@ExperimentalComposeUiApi internal fun Modifier.pointerInteropFilter(view: AndroidViewHolder): Modifier { val filter = PointerInteropFilter() filter.onTouchEvent = { motionEvent -> // 1、分发事件 view.dispatchTouchEvent(motionEvent) } ... return this.then(filter) } 复制代码
AndroidView 的 pointFilter 实现是 PointerInteropFilter,并且,我们看到了很熟悉的 dispatchTouchEvent 代码,在 PointerInteropFilter 中会回调 onTouchEvent,我们看下分发事件时,响应的 PointerInteropFilter.onPointerEvent 方法
override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize ) { ... if (state !== DispatchToViewState.NotDispatching) { if (pass == PointerEventPass.Initial && dispatchDuringInitialTunnel) { // 1、分发事件 dispatchToView(pointerEvent) } ... } private fun dispatchToView(pointerEvent: PointerEvent) { ,,, val changes = pointerEvent.changes if (changes.fastAny { it.anyChangeConsumed() }) { // 处理 cancel 事件 ... } else { // 2、将 pointerEvent 转成 Android 的 MotionEvent 对象 pointerEvent.toMotionEventScope( ... ) { motionEvent -> if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { // 3、触发 onTouch 回调 state = if (onTouchEvent(motionEvent)) { ... } else { onTouchEvent(motionEvent) } } ... 复制代码
- 判断时间状态
- 将 pointerEvent 转成 Android 的 MotionEvent 对象
- 触发 onTouch 回调,这时候就会回调 view.dispatchTouchEvent(motionEvent) 方法
总结
至此,Compose 的事件分发流程已梳理完毕。其实,里面还有很多细节点还是没有讲解清楚,但止于篇幅太长,后面再重新开篇梳理细节点