Compose 事件分發(fā)(下) 分發(fā)觸摸點(diǎn)

在上一篇 《Compose 事件分發(fā)(上) 尋找觸摸點(diǎn)》中已經(jīng)介紹,在觸摸 compose 組件時(shí),會(huì)從根節(jié)點(diǎn)開(kāi)始遍歷,獲取命中的 PointerInputFilter,然后對(duì)其進(jìn)行事件分發(fā),今天,我們來(lái)重點(diǎn)講解一下事件的分發(fā)過(guò)程,并且在 AndroidView 上,嵌套原生 View 的時(shí)候,事件的分發(fā)過(guò)程

一、示例

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)
                    ){}
           }      
 }

這次我們的示例更改一下,添加兩個(gè)帶有 pointInput 的組件 Box 和 Row,以便更好的查看事件響應(yīng)。

二、分析

1、Compose 組件事件分發(fā)分析

繼續(xù)回到 pointerInputEventProcessor.process 方法:

@OptIn(InternalCoreApi::class)
// 1、root 為 AndroidComposeView 傳進(jìn)來(lái)的根節(jié)點(diǎn)
internal class PointerInputEventProcessor(val root: LayoutNode) {
  ...
  fun process(
        pointerEvent: PointerInputEvent,
        positionCalculator: PositionCalculator
    ): ProcessResult {
      // 收集 PointerInputFilter 集
       
      // 6、分發(fā)事件 Dispatch to PointerInputFilters
      val dispatchedToSomething = hitPathTracker.dispatchChanges(internalPointerEvent)
       .... 
        return ProcessResult(dispatchedToSomething, anyMovementConsumed)
    }

我們來(lái)查看下 dispatchChanges 方法:

 fun dispatchChanges(internalPointerEvent: InternalPointerEvent): Boolean {
       //  1、遍歷子節(jié)點(diǎn),分發(fā) main 事件
        var dispatchHit = root.dispatchMainEventPass(
            internalPointerEvent.changes,
            rootCoordinates,
            internalPointerEvent
        )
      // 2、遍歷子節(jié)點(diǎn),分發(fā) final 事件
        dispatchHit = root.dispatchFinalEventPass() || dispatchHit
        return dispatchHit
  }

這里的 root 再介紹一下,引用上文:

將 hitResult 集合設(shè)置到 hitPathTracker 中,內(nèi)部會(huì)對(duì) hitResult 集合轉(zhuǎn)成 Node 鏈表,在分發(fā)時(shí)會(huì)遍歷該鏈表,需要注意的是,這個(gè)鏈表的順序是從 parent layoutNode 到 child LayoutNode 的順序,跟 view 分發(fā)一致

  1. 遍歷子節(jié)點(diǎn),本質(zhì)就是遍歷 pointInput,分發(fā) main 事件

  2. 遍歷子節(jié)點(diǎn),本質(zhì)就是遍歷 pointInput,分發(fā) final 事件

來(lái)看下 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、分發(fā) Initial 事件
            pointerInputFilter.onPointerEvent(event, PointerEventPass.Initial, size)

            // Dispatch to children.
            if (pointerInputFilter.isAttached) {
               // 2、繼續(xù)遍歷子節(jié)點(diǎn)遞歸分發(fā)
                children.forEach {it.dispatchMainEventPass(relevantChanges,coordinates!!, internalPointerEvent)}
            }

            if (pointerInputFilter.isAttached) {
                //   3、分發(fā) Main 事件
                pointerInputFilter.onPointerEvent(event, PointerEventPass.Main, size)
            }
        }
    }

Compose 對(duì)一個(gè)事件分了三種類(lèi)型,目的是更好的處理事件,翻譯自注釋?zhuān)?/p>

  • Initial :允許祖先在后代之前使用 PointerInputChange 的各個(gè)方面。例如,滾動(dòng)條可能會(huì)阻止按鈕在滾動(dòng)開(kāi)始后被其他手指點(diǎn)擊
  • Main :手勢(shì)過(guò)濾器應(yīng)該對(duì) PointerInputChanges 的各個(gè)方面做出反應(yīng)和使用的主要通道。這是后代將在父母之前與 PointerInputChanges 交互的主要路徑。這允許按鈕在底部的容器響應(yīng)點(diǎn)擊之前響應(yīng)點(diǎn)擊。
  • Final :在這個(gè)過(guò)程中,后代可以了解在 Main 過(guò)程中祖先使用了 PointerInputChanges 的哪些方面。例如,這是一個(gè)按鈕如何確定它不應(yīng)再響應(yīng)手指離開(kāi)它的方式,因?yàn)楦笣L動(dòng)條已經(jīng)消耗了 PointerInputChange 中的移動(dòng)。

為了不陷入源碼調(diào)用陷阱,這里結(jié)合示例用圖表示調(diào)用過(guò)程:

Main 會(huì)對(duì)事件進(jìn)行消費(fèi)處理,這也是為什么子組件優(yōu)先消費(fèi)事件的原因,也即示例 demo 中,如果我們點(diǎn)擊 Row 區(qū)域的話(huà),響應(yīng)的是 Row,而不是 Box。

事件的消費(fèi)處理,是調(diào)用 pointInput 設(shè)置的 pointerInputFilter 的 onPointerEvent 方法,我們需要回到示例 demo,找到 pointInput,進(jìn)入源碼探索:

fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
): Modifier = composed(
    ...
) {
    val density = LocalDensity.current
    val viewConfiguration = LocalViewConfiguration.current
   // 1、pointerInputFilter 的實(shí)現(xiàn)類(lèi)是 SuspendingPointerInputFilter
    remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply {
        LaunchedEffect(this, key1) {
           // 2、啟用掛起函數(shù),block 為示例 demo 中的 detectTapGestures
            block()
        }
    }
}

這里我們需要關(guān)注兩個(gè)點(diǎn):

  • pointerInputFilter 的實(shí)現(xiàn)類(lèi)是 SuspendingPointerInputFilter,我們需要進(jìn)入到該類(lèi)查看 onPointerEvent 的調(diào)用
  • 利用 LaunchedEffect,從可組合項(xiàng)內(nèi)安全調(diào)用掛起函數(shù),block 為示例中設(shè)置的 detectTapGestures 掛起函數(shù),需要注意的是,block 是在 apply 于 SuspendingPointerInputFilter 作用域內(nèi)的,后面的擴(kuò)展函數(shù)會(huì)調(diào)用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法

detectTapGestures 可以理解成是訂閱者,SuspendingPointerInputFilter 為事件的發(fā)布者,在 SuspendingPointerInputFilter 收到事件調(diào)用 onPointerEvent 方法時(shí),會(huì)觸發(fā)該訂閱者,訂閱者處理事件是否消費(fèi),并且還可以處理是單擊、雙擊還是長(zhǎng)按,然后回調(diào)自己的各個(gè)函數(shù)。

我們先來(lái)看下事件的發(fā)布者 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、發(fā)布者會(huì)調(diào)用該方法來(lái)創(chuàng)建一個(gè)協(xié)程,并添加到 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 ,觸發(fā) 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、判斷事件類(lèi)型是否是 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. 發(fā)布者會(huì)調(diào)用該方法來(lái)創(chuàng)建一個(gè)協(xié)程,并添加到 pointerHandlers 集合中
  2. 遍歷 pointerHandlers 的 offerPointerEvent 方法發(fā)布事件
  3. 判斷事件類(lèi)型是否是 Main 事件
  4. 判斷 pointerAwaiter 是否為空,如果不為空的話(huà),則恢復(fù)掛起函數(shù)
  5. 掛起函數(shù)的注冊(cè),對(duì) pointerAwaiter 進(jìn)行賦值

然后我們?cè)俑M(jìn) 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、事件轉(zhuǎn)換后最終結(jié)果,通過(guò) channel 來(lái)阻塞等待結(jié)果的返回,例如會(huì)回調(diào) onDoubleTap、onLongPress 等
    }
   // 2、遍歷手勢(shì)
   forEachGesture {
        // 3、調(diào)用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法,創(chuàng)建并注冊(cè)個(gè)協(xié)程
        awaitPointerEventScope {
            // 4、處理最終事件消費(fèi)的地方,然后將事件處理的最終結(jié)果發(fā)送至 channel
            translatePointerEventsToChannel(  this@coroutineScope,channel,consumeOnlyDownsSignal, consumeAllUntilUpSignal)
        }
    }

  1. 事件轉(zhuǎn)換后最終結(jié)果,通過(guò) channel 來(lái)阻塞等待結(jié)果的返回,例如會(huì)回調(diào) onDoubleTap、onLongPress 等
  2. 遍歷手勢(shì),內(nèi)部會(huì)執(zhí)行 block,并且會(huì)掛起等待所有的 Final 事件結(jié)束
  3. 調(diào)用 SuspendingPointerInputFilter 的 awaitPointerEventScope 方法,創(chuàng)建啟動(dòng)并注冊(cè)個(gè)協(xié)程
  4. 處理最終事件消費(fèi)的地方,然后將事件處理的最終結(jié)果發(fā)送至 channel

現(xiàn)在我們也大致理解了整體過(guò)程,這里還是通過(guò)繪制圖來(lái)總結(jié),避免代碼太多干擾思路:

由于內(nèi)容篇幅太長(zhǎng),這里只對(duì) down 事件進(jìn)行講解,進(jìn)入 translatePointerEventsToChannel:

private suspend fun AwaitPointerEventScope.translatePointerEventsToChannel(
    scope: CoroutineScope,
    channel: SendChannel<TapGestureEvent>,
    detectDownsOnly: State<Boolean>,
    consumeAllUntilUp: MutableState<Boolean>
) {
    ...
   // 1、判斷事件有無(wú)消費(fèi),如果沒(méi)有消費(fèi)的話(huà)則進(jìn)入
    else if (event.changes.fastAll { it.changedToDown() }) {
      //  2、從 event 中取出事件
      val change = event.changes[0]
      // 3、消費(fèi) down 事件,其實(shí)就是設(shè)置 consumed.downChange = true
      change.consumeDownChange()
      // 4、將 down 結(jié)果通過(guò) channel 發(fā)送出去
      channel.trySend(Down(change.position, change.uptimeMillis))
    }
  ...
}

  1. 判斷事件有無(wú)消費(fèi),如果沒(méi)有消費(fèi)的話(huà)則進(jìn)入
  2. 從 PointerEvent 中取出事件
  3. 消費(fèi) down 事件,其實(shí)就是設(shè)置 consumed.downChange = true
  4. 將 down 結(jié)果通過(guò) channel 發(fā)送出去

消費(fèi) down 事件時(shí)標(biāo)記 downChage 為 true 很重要,因?yàn)槲覀兊?pointerInputFilter 有 2 個(gè),并且在處理 Main 事件時(shí),是從子組件往父組件開(kāi)始遍歷,也即子組件會(huì)先消費(fèi)事件,在消費(fèi)了事件之后,遍歷到父組件時(shí),則進(jìn)入不了這個(gè)判斷,也就不處理。

2、AndroidView 組件事件分發(fā)分析

通過(guò)上面的分析知道,Compose 組件是通過(guò) SuspendingPointerInputFilter 實(shí)現(xiàn)事件的處理,那 AndroidView 組件是怎么分發(fā)的呢?繼續(xù)貼一下之前的圖:

我們可以直接看下 AndroidViewHolder,在返回的 layoutNode 中,有預(yù)設(shè)一個(gè) 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)

進(jìn)入 pointerInteropFilter 查看代碼:

@ExperimentalComposeUiApi
internal fun Modifier.pointerInteropFilter(view: AndroidViewHolder): Modifier {
    val filter = PointerInteropFilter()
    filter.onTouchEvent = { motionEvent ->
        // 1、分發(fā)事件
        view.dispatchTouchEvent(motionEvent)
    }
    ...
    return this.then(filter)
}

AndroidView 的 pointFilter 實(shí)現(xiàn)是 PointerInteropFilter,并且,我們看到了很熟悉的 dispatchTouchEvent 代碼,在 PointerInteropFilter 中會(huì)回調(diào) onTouchEvent,我們看下分發(fā)事件時(shí),響應(yīng)的 PointerInteropFilter.onPointerEvent 方法

   override fun onPointerEvent(
                pointerEvent: PointerEvent,
                pass: PointerEventPass,
                bounds: IntSize ) {
     ...
      if (state !== DispatchToViewState.NotDispatching) {
        if (pass == PointerEventPass.Initial && dispatchDuringInitialTunnel) {
           // 1、分發(fā)事件
            dispatchToView(pointerEvent)
        }
       ...
   }

private fun dispatchToView(pointerEvent: PointerEvent) {
   ,,,
   val changes = pointerEvent.changes
   if (changes.fastAny { it.anyChangeConsumed() }) {
      //  處理 cancel 事件
       ...
   } else {
     // 2、將 pointerEvent 轉(zhuǎn)成 Android 的 MotionEvent 對(duì)象
       pointerEvent.toMotionEventScope(
         ...
       ) { motionEvent ->
          if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
              // 3、觸發(fā) onTouch 回調(diào)
               state = if (onTouchEvent(motionEvent)) {
                    ...
              } else {
                 onTouchEvent(motionEvent)
            }
         }
         ...

  1. 判斷時(shí)間狀態(tài)
  2. 將 pointerEvent 轉(zhuǎn)成 Android 的 MotionEvent 對(duì)象
  3. 觸發(fā) onTouch 回調(diào),這時(shí)候就會(huì)回調(diào) view.dispatchTouchEvent(motionEvent) 方法

總結(jié)

至此,Compose 的事件分發(fā)流程已梳理完畢。其實(shí),里面還有很多細(xì)節(jié)點(diǎn)還是沒(méi)有講解清楚,但止于篇幅太長(zhǎng),后面再重新開(kāi)篇梳理細(xì)節(jié)點(diǎn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容