通过调用栈快速探究 Compose 中 touch 事件的处理原理

Compose-base-touch.png

前言

Compose 视图的处理方式和 Android 传统 View 有很大差别,针对 touch 事件的处理自然也截然不同。

如何在 Compose 中处理 touch 事件,官方已有非常详尽的说明,可以参考:https://developer.android.google.cn/jetpack/compose/touch-input

本文将以 Compose 中几种最典型的 touch 处理为例,分别介绍其使用场景,并打印其调用栈。最后结合栈和 touch 源码,一起综合分析 Compose 中处理 touch 的原理细节。

各种 touch 处理的写法和场景

pointerInput

Compose 中处理所有手势事件的入口,类似传统视图的 onTouch。在这里可以识别 click 手势,而且相应优先级高于 clickable。

第二个参数为 PointerInputScope 的扩展函数类型,有如下:

  • 来自 TapGestureDetector 文件中定义的 detectTapGestures:可以用来检测 onDoubleTap、onLongPress、onPress、onTap 几种手势
  • 来自 DragGestureDetector 文件中定义的 detectDragGestures:可以用来检测拖拽开始、结束、取消等手势
  • 来自 TransformGestureDetector 文件中定义的 detectTransformGestures:可以用来检测旋转、平移、缩放的手势
  • 等等
 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
   
   
     Column(
         modifier = Modifier
             ...
             .pointerInput(Unit) {
   
   
                 detectTapGestures(
                     onDoubleTap = {
   
    },
                     onLongPress = {
   
    },
                     onPress = {
   
    },
                     onTap = {
   
    }
                 )detectDragGestures(
                     onDragStart = {
   
    },
                     onDragEnd = {
   
    },
                     onDragCancel = {
   
    },
                     onDrag = {
   
    change: PointerInputChange, dragAmount: Offset -> 
                         // Todo
                     }
                 )
 ​
                 detectTransformGestures {
   
    centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
                     // Todo
                 }
             }
     ) {
   
   
         ...
     }
 }

我们在 pointerInput 里一进来加上 log,

 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
   
   
     Column(
         modifier = Modifier
             .pointerInput(Unit) {
   
   
                 LogUtil.printLog(message = "GameScreen pointerInput", throwable = Throwable())
             }
     )
 }

打印其调用栈:

 GameScreen pointerInput
 java.lang.Throwable
     at com.ellison.flappybird.view.GameScreenKt$GameScreen$3.invokeSuspend(GameScreen.kt:51)
     ...androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invokeSuspend(SuspendingPointerInputFilter.kt:562)
     at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invoke(Unknown Source:8)
     at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invoke(Unknown Source:4)
     ...
     at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:561)
     at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
     at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
     ...
     at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)
     at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)
     at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)
     at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)
     at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)
     at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)
     ...

pointerInteropFilter

pointerInteropFilter 可以用来直接处理 ACTION DOWN、MOVE、UP 和 CANCEL 事件的函数,类似 onTouchEvent(),还可以指定是否允许父亲拦截:requestDisallowInterceptTouchEvent

需要留意的是如果 DOWN return 了 false 的话,那么 ACTION_UP 就不会发过来了。

 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
   
   
     Column(
         modifier = Modifier
             .pointerInteropFilter {
   
   
                     when (it.action) {
   
   
                         ACTION_DOWN -> {
   
   
                             LogUtil.printLog(message = "GameScreen pointerInteropFilter ACTION_DOWN status:${
     
     viewState.gameStatus}", throwable = Throwable())
                         }
 ​
                         MotionEvent.ACTION_MOVE -> {
   
   
                             // Todo
                         }
 ​
                         MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
   
   
                             // Todo
                         }
                     }
                     true
                 }
     )
 }

我们在 ACTION_DOWN 里加个 log 看下 stack:

 GameScreen pointerInteropFilter ACTION_DOWN status:Waiting
 java.lang.Throwable
     at com.ellison.flappybird.view.GameScreenKt$GameScreen$4$1.invoke(GameScreen.kt:58)
     at com.ellison.flappybird.view.GameScreenKt$GameScreen$4$1.invoke(GameScreen.kt:53)
     at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$3.invoke(PointerInteropFilter.android.kt:301)
     at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$3.invoke(PointerInteropFilter.android.kt:294)
     at androidx.compose.ui.input.pointer.PointerInteropUtils_androidKt.toMotionEventScope-ubNVwUQ(PointerInteropUtils.android.kt:81)
     at androidx.compose.ui.input.pointer.PointerInteropUtils_androidKt.toMotionEventScope-d-4ec7I(PointerInteropUtils.android.kt:35)
     at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1.dispatchToView(PointerInteropFilter.android.kt:294)
     at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1.onPointerEvent-H0pRuoY(PointerInteropFilter.android.kt:229)
     at androidx.compose.ui.node.BackwardsCompatNode.onPointerEvent-H0pRuoY(BackwardsCompatNode.kt:365)
 ​
     at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
     at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
     ...
     at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)
     at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)
     at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)
     at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)
     at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)
     at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)
     ...

combinedClickable

综合单击、双击、长按三种点击事件的处理函数,但至少需要指定处理单击 onClick 的 lambda。

如果同时设置了 pointerInteropFilter 并返回 true 的话,那么 combinedClickable Unit 就不会被处理了。

 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
   
   
     Column(
         modifier = Modifier
             .combinedClickable(
                 onLongClick = {
   
    },
                 onDoubleClick = {
   
    },
                 onClick = {
   
   
                     LogUtil.printLog(message = "GameScreen combinedClickable onClick", throwable = Throwable())
                 }
             )
     )
 }

同样在最基本的 onClick 里打印个 stack:

 GameScreen combinedClickable onClick
 java.lang.Throwable
     at com.ellison.flappybird.view.GameScreenKt$GameScreen$4.invoke(GameScreen.kt:56)
     at com.ellison.flappybird.view.GameScreenKt$GameScreen$4.invoke(GameScreen.kt:45)
     at androidx.compose.foundation.CombinedClickablePointerInputNode$pointerInput$5.invoke-k-4lQ0M(Clickable.kt:939)
     at androidx.compose.foundation.CombinedClickablePointerInputNode$pointerInput$5.invoke(Clickable.kt:927)
     at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapGestures$2$1.invokeSuspend(TapGestureDetector.kt:144)
     ...
     at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
     at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine$withTimeout$job$1.invokeSuspend(SuspendingPointerInputFilter.kt:724)
     at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
     at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
     at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
     at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
     at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
     ...

clickable

clickable 算是最简单的设置 click 回调的办法。

需要了留意的是:

  1. 当同时设置了 combinedClickable 的 onClick 的话,clickable 就不会被调用了
  2. 当同时设置了 pointerInteropFilter 并返回 true 的话,和 combinedClickable 一样,clickable 就不会处理了
 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
   
   
     Column(
         modifier = Modifier
             .clickable {
   
   
                 LogUtil.printLog(message = "GameScreen clickable", throwable = Throwable())
             }
     )
 }

直接打个 stack:

 GameScreen clickable
 java.lang.Throwable
     at com.ellison.flappybird.view.GameScreenKt$GameScreen$1.invoke
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值