Compose 事件分发(下) 分发触摸点

在上一篇 《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 分发一致

  1. 遍历子节点,本质就是遍历 pointInput,分发 main 事件

  2. 遍历子节点,本质就是遍历 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
        }
  }

  1. 发布者会调用该方法来创建一个协程,并添加到 pointerHandlers 集合中
  2. 遍历 pointerHandlers 的 offerPointerEvent 方法发布事件
  3. 判断事件类型是否是 Main 事件
  4. 判断 pointerAwaiter 是否为空,如果不为空的话,则恢复挂起函数
  5. 挂起函数的注册,对 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)
        }
    }

  1. 事件转换后最终结果,通过 channel 来阻塞等待结果的返回,例如会回调 onDoubleTap、onLongPress 等
  2. 遍历手势,内部会执行 block,并且会挂起等待所有的 Final 事件结束
  3. 调用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法,创建启动并注册个协程
  4. 处理最终事件消费的地方,然后将事件处理的最终结果发送至 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))
    }
  ...
}

  1. 判断事件有无消费,如果没有消费的话则进入
  2. 从 PointerEvent 中取出事件
  3. 消费 down 事件,其实就是设置 consumed.downChange = true
  4. 将 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)
            }
         }
         ...

  1. 判断时间状态
  2. 将 pointerEvent 转成 Android 的 MotionEvent 对象
  3. 触发 onTouch 回调,这时候就会回调 view.dispatchTouchEvent(motionEvent) 方法

总结

至此,Compose 的事件分发流程已梳理完毕。其实,里面还有很多细节点还是没有讲解清楚,但止于篇幅太长,后面再重新开篇梳理细节点

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容