【问题标题】:How can I get onTouchEvent in Jetpack Compose?如何在 Jetpack Compose 中获取 onTouchEvent?
【发布时间】:2020-10-28 11:31:35
【问题描述】:

正常情况下,我们可以有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

【问题讨论】:

    标签: android android-jetpack-compose


    【解决方案1】:

    为此,我们有一个单独的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 示例中。

    附:不要忘记@ExperimentalPointerInput 注释。

    【讨论】:

    • 很好,很可爱!测试pointerInteropFilter,就像一个魅力。
    • 虽然我没有使用@ExperimentalPointerInput.. 嗯,还需要吗?
    • @Elye 好吧,我正在使用 androidx.compose.ui:ui:1.0.0-alpha05pointerInput 需要 ExperimentalPointerInput 注释。也许他们会改变一些细节,不确定。很高兴听到它对你有用!
    • beta9中没有@ExperimentalPointerInput,改用@ExperimentalComposeUiApi
    • 在 Compose 1.1.0 被标记为@OptIn(ExperimentalComposeUiApi::class)
    【解决方案2】:

    pointerInteropFilter 未被描述为如果您不使用与现有视图代码互操作的触摸 api 的首选方式。

    一个特殊的 PointerInputModifier 提供对底层的访问 MotionEvents 最初分派给 Compose。首选指针输入和 仅将其用于与消耗的现有代码的互操作 运动事件。虽然此修饰符的主要目的是允许 任意代码来访问派发到的原始 MotionEvent Compose,为了完整起见,提供了类似物以允许任意 与系统交互的代码,就像它是一个 Android 视图一样。

    您可以使用pointerInput awaitTouchDown 用于MotionEvent.ACTION_DOWNawaitPointerEvent 用于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. 如果孩子和父母正在监听输入变化,它会传播 从内在的孩子到外在的父母。与来自的触摸事件不同 父母对孩子
    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(),调用 awaitPointerEvent之后的pointerInputChange.consumePositionChange()

      并且awaitFirstDown() 具有requireUnconsumed 参数,即 默认情况下为真。如果您将其设置为 false 即使指针输入消耗 在你的手势之前下来,你仍然得到它。这也是诸如拖动之类的事件如何使用它来首先向下移动。

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

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

    【讨论】:

      【解决方案3】:

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

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

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

      ps:1.0.0-beta03

      【讨论】:

        【解决方案4】:

        经过一番研究,貌似可以用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 确实有助于提供最后一个位置(如果需要)

        【讨论】:

          猜你喜欢
          • 2020-03-03
          • 2022-11-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-12-25
          • 2022-01-16
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多