如何在 Jetpack Compose 中获取 onTouchEvent?

Ely*_*lye 7 android android-jetpack-compose

在正常情况下,我们可以有 onTouchEvent

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {}
            MotionEvent.ACTION_MOVE -> {}
            MotionEvent.ACTION_UP -> {}
            else -> return false
        }
        invalidate()
        return true
    }
Run Code Online (Sandbox Code Playgroud)

在 Jetpack Compose 中,我只能发现我们有tapGestureFilterin 修饰符,它只执行来自ACTION_UPonly的动作。

Modifier
    .tapGestureFilter { Log.d("Track", "Tap ${it.x} | ${it.y}") }
    .doubleTapGestureFilter { Log.d("Track", "DoubleTap ${it.x} | ${it.y}") }
Run Code Online (Sandbox Code Playgroud)

onTouchEventJetpack Compose是否有等价物?

Thr*_*ian 28

pointerInteropFilter如果您不使用 touch api 与现有视图代码进行互操作,则未将其描述为首选使用方式。

一个特殊的 PointerInputModifier,提供对最初调度到 Compose 的底层 MotionEvent 的访问。优先选择pointerInput,并且仅将其用于与使用MotionEvents 的现有代码的互操作。虽然此修饰符的主要目的是允许任意代码访问分派给 Compose 的原始 MotionEvent,但为了完整性,提供了类似物以允许任意代码与系统交互,就好像它是 Android 视图一样。

您可以使用pointerInput awaitTouchDownfor MotionEvent.ACTION_DOWNawaitPointerEventforMotionEvent.ACTION_MOVEMotionEvent.ACTION_UP

val pointerModifier = Modifier
    .pointerInput(Unit) {
        awaitEachGesture {

            awaitFirstDown()
            // ACTION_DOWN here

            do {

                //This PointerEvent contains details including
                // event, id, position and more
                val event: PointerEvent = awaitPointerEvent()
                // ACTION_MOVE loop

                // Consuming event prevents other gestures or scroll to intercept
                event.changes.forEach { pointerInputChange: PointerInputChange ->
                    pointerInputChange.consumePositionChange()
                }
            } while (event.changes.any { it.pressed })

            // ACTION_UP is here


        }
    }
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

关于手势的一些要点

  1. 指针输入传播是指有多个从底部到顶部的输入。
  2. 如果孩子和父母正在监听输入更改,它会从内部孩子传播到外部,然后传播到父母。与从父级到子级的触摸事件不同 (这适用于 PointerEventPass.Main、PointerEventPass.Final 或 PointerEventPass.Initial 更改传播方向)
  3. 如果您不消耗事件,则其他事件(例如滚动拖动)可能会干扰或消耗事件,大多数事件在传播给它们之前会检查它是否已被消耗

detectorDragGestures源码实例

val down = awaitFirstDown(requireUnconsumed = false)
    var drag: PointerInputChange?
    var overSlop = Offset.Zero
    do {
        drag = awaitPointerSlopOrCancellation(
            down.id,
            down.type
        ) { change, over ->
            change.consumePositionChange()
            overSlop = over
        }
    } while (drag != null && !drag.positionChangeConsumed())
Run Code Online (Sandbox Code Playgroud)
  1. 所以当你需要阻止其他事件拦截时

    调用pointerInputChange.consumeDown()after awaitFirstDown,调用pointerInputChange.consumePositionChange() afterawaitPointerEvent

    并且awaitFirstDown()requireUnconsumed默认为 true 的参数。如果你将其设置为 false,即使pointerInput 在你的手势之前消耗殆尽,你仍然会得到它。这也是像拖动这样的事件如何利用它来首先下降的方式。

  2. 您看到的每个可用事件detectDragGesturesdetectTapGestures甚至awaitFirstDown用于awaitPointerEvent 实现,因此使用awaitFirstDown使用更改您可以配置自己的手势。awaitPointerEvent

例如,这是我从原始版本定制的函数,detectTransformGestures仅使用特定数量的向下指针来调用。

suspend fun PointerInputScope.detectMultiplePointerTransformGestures(
    panZoomLock: Boolean = false,
    numberOfPointersRequired: Int = 2,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,

    ) {
    forEachGesture {
        awaitPointerEventScope {
            var rotation = 0f
            var zoom = 1f
            var pan = Offset.Zero
            var pastTouchSlop = false
            val touchSlop = viewConfiguration.touchSlop
            var lockedToPanZoom = false

            awaitFirstDown(requireUnconsumed = false)

            do {
                val event = awaitPointerEvent()

                val downPointerCount = event.changes.size

                // If any position change is consumed from another pointer or pointer
                // count that is pressed is not equal to pointerCount cancel this gesture
                val canceled = event.changes.any { it.positionChangeConsumed() } || (
                        downPointerCount != numberOfPointersRequired)

                if (!canceled) {
                    val zoomChange = event.calculateZoom()
                    val rotationChange = event.calculateRotation()
                    val panChange = event.calculatePan()

                    if (!pastTouchSlop) {
                        zoom *= zoomChange
                        rotation += rotationChange
                        pan += panChange

                        val centroidSize = event.calculateCentroidSize(useCurrent = false)
                        val zoomMotion = abs(1 - zoom) * centroidSize
                        val rotationMotion =
                            abs(rotation * PI.toFloat() * centroidSize / 180f)
                        val panMotion = pan.getDistance()

                        if (zoomMotion > touchSlop ||
                            rotationMotion > touchSlop ||
                            panMotion > touchSlop
                        ) {
                            pastTouchSlop = true
                            lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
                        }
                    }

                    if (pastTouchSlop) {
                        val centroid = event.calculateCentroid(useCurrent = false)
                        val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
                        if (effectiveRotation != 0f ||
                            zoomChange != 1f ||
                            panChange != Offset.Zero
                        ) {
                            onGesture(centroid, panChange, zoomChange, effectiveRotation)
                        }
                        event.changes.forEach {
                            if (it.positionChanged()) {
                                it.consumeAllChanges()
                            }
                        }
                    }
                }
            } while (!canceled && event.changes.any { it.pressed })
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑

1.2.0-beta01开始,不推荐使用部分消耗,例如 PointerInputChange.consemePositionChange()PointerInputChange.consumeDownChange()和用于消耗所有更改的消耗PointerInputChange.consumeAllChanges()

PointerInputChange.consume()
Run Code Online (Sandbox Code Playgroud)

是唯一用于防止其他手势/事件的方法。

这里还有一个教程,详细介绍了手势


Yur*_*sap 12

我们有一个单独的,这非常有用。有两个主要的扩展功能适合您:

如果您想处理和处理事件,我建议使用pointerInteropFilter它的类似物View.onTouchEvent. 它与modifier以下一起使用:

Column(modifier = Modifier.pointerInteropFilter {
    when (it.action) {
        MotionEvent.ACTION_DOWN -> {}
        MotionEvent.ACTION_MOVE -> {}
        MotionEvent.ACTION_UP -> {}
        else ->  false
    }
     true
})
Run Code Online (Sandbox Code Playgroud)

这将是根据您指定的View.onTouchEvent示例编写调整后的代码。

PS 不要忘记@ExperimentalPointerInput注释。

  • 这个解决方案不是很好,因为你不能触发子视图的任何手势事件,所以子视图的触发取决于你在“when”语句后返回的内容(true或false),如果你返回false,子按钮会触发,但你之后无法触发该视图的ACTION_MOVE和ACTION_UP。我正在寻找可以触发两者的解决方案。 (3认同)