Как я могу получить onTouchEvent в 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}") }

Есть ли эквивалент onTouchEvent для Jetpack Compose?

5 ответов

Решение

Для этого у нас есть отдельный пакет, который очень полезен. Есть две основные функции расширения, которые вам подойдут:

Если вы хотите обработать и обработать событие, я рекомендую использовать 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 образец.

PS Не забывайте про @ExperimentalPointerInput аннотация.

pointerInteropFilterне описывается как предпочтительный способ использования, если вы не используете сенсорный API с взаимодействием с существующим кодом просмотра.

Специальный PointerInputModifier, предоставляющий доступ к базовым событиям MotionEvents, изначально отправленным в Compose. Отдавайте предпочтение pointerInput и используйте его только для взаимодействия с существующим кодом, использующим MotionEvents. Хотя основной целью этого модификатора является предоставление произвольному коду доступа к исходному MotionEvent, отправленному в Compose, для полноты предоставлены аналоги, позволяющие произвольному коду взаимодействовать с системой, как если бы это было Android View.

Вы можете использовать pointInputFilter, awaitTouchDownза MotionEvent.ACTION_DOWN, а также awaitPointerEventза MotionEvent.ACTION_MOVEа также MotionEvent.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
            }
        }
}

Может быть, немного поздно, но поскольку compose постоянно обновляется, на сегодняшний день я делаю это следующим образом:

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

У нас также есть detectHorizontalDragGestures а также detectVerticalDragGestures среди прочего, чтобы помочь нам.

пс: 1.0.0-beta03

СочинитьPointerEventType.Press,.Moveи.Releaseблизко соответствует просмотруMotionEvent.ACTION_DOWN,_MOVEи_UPсоответственно. (См. файл реализации платформы Android PointerEvent.android.kt для сопоставления сPointerEvent.type.)

Вы можете расширить функцию, которая принимает три лямбда-параметра и направляет события в нужный.

В следующем примере предполагается, что в лямбда-выражениях обрабатываются несколько указателей.

      suspend fun PointerInputScope.routePointerChangesTo(
    onDown: (PointerInputChange) -> Unit = {},
    onMove: (PointerInputChange) -> Unit = {},
    onUp: (PointerInputChange) -> Unit = {}
) {
    awaitEachGesture {
        do {
            val event = awaitPointerEvent()
            event.changes.forEach {
                when (event.type) {
                    PointerEventType.Press -> onDown(it)
                    PointerEventType.Move -> onMove(it)
                    PointerEventType.Release -> onUp(it)
                }
                it.consume()
            }
        } while (event.changes.any { it.pressed })
    }
}

Проверьте это, позвонив с помощьюModifierвот так наComposable:

          Modifier.pointerInput(Unit) {
        routePointerChangesTo(
            onDown = { change -> println("Down at ${change.position}")},
            onMove = { change -> println("Move to ${change.position}")},
            onUp = { change -> println("Up at ${change.position}")}
        )
    }

Если у тебя все в порядке сonDown()вызывается только после превышения скорости касания, вы можете просто использоватьdetectDragGestures()функция включенаPointerInputScopeдля достижения аналогичного результата.

Если следует игнорировать указатели, отличные от первого, все становится немного сложнее:

      suspend fun PointerInputScope.routePointerChangesTo(
    onDown: (PointerInputChange) -> Unit = {},
    onMove: (PointerInputChange) -> Unit = {},
    onUp: (PointerInputChange) -> Unit = {}
) {
    awaitEachGesture {
        awaitFirstDown().let { firstDownChange ->
            onDown(firstDownChange)
            firstDownChange.consume()
            var isFirstPointerDown = true
            do {
                awaitPointerEvent().let { event ->
                    event.changes.forEach { change ->
                        if (change.id == firstDownChange.id) {
                            when (event.type) {
                                PointerEventType.Move -> {
                                    onMove(change)
                                    change.consume()
                                }
                                PointerEventType.Release -> {
                                    if (!change.pressed) {
                                        isFirstPointerDown = false
                                        onUp(change)
                                        change.consume()
                                    }
                                }
                            }
                        }
                    }
                }
            } while (isFirstPointerDown)
        }
    }
}

См. также это руководство для видео и других фрагментов использованияpointerInput.

После некоторых исследований, похоже, можно использовать 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 помогает обеспечить последнюю позицию (при необходимости)

Другие вопросы по тегам