如何在 Jetpack Compose 中获取 onTouchEvent?

How can I get onTouchEvent in 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
    }

在 Jetpack Compose 中,我只能发现我们在修饰符中有 tapGestureFilter,它只接受 ACTION_UP 的动作。

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

Jetpack Compose 有等效的 onTouchEvent 吗?

经过一番研究,看起来可以使用 dragGestureFilter,与 tapGestureFilter

混合使用
Modifier
    .dragGestureFilter(object: DragObserver {
        override fun onDrag(dragDistance: Offset): Offset {
            Log.d("Track", "onActionMove ${dragDistance.x} | ${dragDistance.y}")
            return super.onDrag(dragDistance)
        }
        override fun onStart(downPosition: Offset) {
            Log.d("Track", "onActionDown ${downPosition.x} | ${downPosition.y}")
            super.onStart(downPosition)
        }
        override fun onStop(velocity: Offset) {
            Log.d("Track", "onStop ${velocity.x} | ${velocity.y}")
            super.onStop(velocity)
        }
    }, { true })
    .tapGestureFilter {
        Log.d("NGVL", "onActionUp ${it.x} | ${it.y}")
    }

仍然使用 tagGestureFilter 的原因是因为 onStop 不提供位置,而只提供速度,因此 tapGestureFilter 确实有助于提供最后的位置(如果需要)

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

  • pointerInput - docs
  • pointerInteropFilter - docs

如果你想处理事件,我建议使用 pointerInteropFilter,它类似于 View.onTouchEvent。它与 modifier:

一起使用
Column(modifier = Modifier.pointerInteropFilter {
    when (it.action) {
        MotionEvent.ACTION_DOWN -> {}
        MotionEvent.ACTION_MOVE -> {}
        MotionEvent.ACTION_UP -> {}
        else ->  false
    }
     true
})

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

P.S。不要忘记 @ExperimentalPointerInput 注释。

可能有点晚了,但由于 compose 不断更新,所以我今天是这样做的:

Modifier
    .pointerInput(Unit) {
        detectTapGestures {...}
     }
    .pointerInput(Unit) {
        detectDragGestures { change, dragAmount ->  ...}
    })

我们还有 detectHorizontalDragGesturesdetectVerticalDragGestures 等人来帮助我们。

ps: 1.0.0-beta03

如果您不使用触摸 api 与现有视图代码进行互操作,

pointerInteropFilter 未被描述为首选使用方式。

A special PointerInputModifier that provides access to the underlying MotionEvents originally dispatched to Compose. Prefer pointerInput and use this only for interoperation with existing code that consumes MotionEvents. While the main intent of this Modifier is to allow arbitrary code to access the original MotionEvent dispatched to Compose, for completeness, analogs are provided to allow arbitrary code to interact with the system as if it were an Android View.

您可以将 pointerInput awaitTouchDown 用于 MotionEvent.ACTION_DOWN,将 awaitPointerEvent 用于 MotionEvent.ACTION_MOVEMotionEvent.ACTION_UP

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

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

关于手势的一些关键注意事项

  1. pointerInput 传播是当你有多个来自 从下到上。
  2. 如果 children 和 parent 正在侦听它传播的输入更改 从内部 children 到外部 parent。不同于触摸事件 parent 到 children
  3. 如果您不使用事件,则可以使用滚动拖动等其他事件 干扰或消耗事件,大多数事件检查它是否 在传播给他们之前消耗

detectDragGestures实例源代码

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())
  1. 所以当你需要阻止其他事件拦截时

    awaitFirstDown之后调用pointerInputChange.consumeDown(),调用 pointerInputChange.consumePositionChange() 在 awaitPointerEvent

    之后

    awaitFirstDown()requireUnconsumed 参数是 默认情况下为真。如果将其设置为 false,即使 pointerInput 消耗 在你的手势之前放下你仍然明白。这也是诸如拖动之类的事件无论如何都会使用它首先下降的方式。

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

例如,这是我从原始 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 })
        }
    }
}

编辑

1.2.0-beta01 开始,部分消耗类似 PointerInputChange.consemePositionChange(), PointerInputChange.consumeDownChange(),以及一个用于消耗所有更改的 PointerInputChange.consumeAllChanges() 已弃用

PointerInputChange.consume()

是唯一一个用来防止其他 gestures/event.

我还有一个 tutorial here 详细介绍了手势