在可滚动控件内拖动手势

Drag gestures inside scrollable control

我做了一个基于 Canvas 的自定义控件。它使用两个 .pointerInput 修饰符,一个用于检测点击,一个用于检测拖动,因此用户可以切换一列 50 个按钮,方法是一次单击一个按钮或拖动多个按钮以将它们全部设置为一次。它运行良好,现在我想要一个包含许多这样的水平滚动行。直接的问题是,当应用 .horizontalScroll 修饰符时,Row 也会吞下垂直移动,甚至点击,所以尽管我可以滚动浏览许多控件,但我不再能够与它们交互。

我能找到的唯一类似的例子是 Gestures 文档中的嵌套滚动,但这是在使用滚动的两个控件之间,虽然外部控件显然不会阻止内部控件接收事件,但它不会清楚如何在我的案例中应用它。

在不粘贴大量代码的情况下,我通过

定义行
@Composable
fun ScrollBoxes() {
    Row(
        modifier = Modifier
            .background(Color.LightGray)
            .fillMaxSize()
            .horizontalScroll(rememberScrollState())
    ) {
        repeat(20) {
            Column {
                Text(
                    "Item $it", modifier = Modifier
                        .padding(2.dp)
                        .width(500.dp)
                )
                JetwashSlide(
                    model = JetwashSlideViewModel()
                )
            }
            
        }
    }
}

而 Canvas 的修饰符是我的自定义控件设置为

modifier
        .pointerInput(Unit) {
            detectDragGestures(
                onDragStart = { ... }

                },
                onDragEnd = { ... },
                onDrag = { change, dragAmount ->
                    change.consumeAllChanges();
                    ...
                    }
                }
            )
        }
        .pointerInput(Unit) {
            detectTapGestures(
                onPress = { it ->
                    ...
                }
            )
        }

一种粗略的方法是拥有一行可滚动的标签,并使用当前选择的标签来确定当前可见的全宽自定义控件。这不会像让控件水平滚动那样美观。有人知道如何实现吗?

好的,我的起点是关于如何从头开始制作滚动寻呼机的教程;

https://fvilarino.medium.com/creating-a-viewpager-in-jetpack-compose-332d6a9181a5

使用它,您可以获得控件实例的水平滚动行。问题是我希望水平滑动由寻呼机处理,点击和垂直滑动由自定义控件处理,虽然自定义控件在控件上获得手势,但它无法将它们传递给如果它们是水平滑动,则寻呼机,因此您只能使用自定义控件之间的间隙进行滚动。我希望控件可以使用 .detectVerticalDragGestures 和 .detectTapGestures,寻呼机可以使用 .detectHorizo​​ntalDragGestures,但事情并没有那么简单。

我最终将 Jetpack 源代码中的代码提取到我自己的代码中,这样我就可以对其进行修改以生成我自己的手势检测器,该检测器捕获垂直滚动和点击事件但不捕获水平滚动;

suspend fun PointerInputScope.detectVerticalDragOrTapGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
    onClick: ((Offset) -> Unit) = { },
    model: JetwashSlideViewModel
) {
    forEachGesture {
        awaitPointerEventScope {
            model.inhibitGesture = false
            val down = awaitFirstDown(requireUnconsumed = false)
            //val drag = awaitVerticalTouchSlopOrCancellation(down.id, onVerticalDrag)
            val drag = awaitTouchSlopOrCancellation(
                down.id
            ) { change: PointerInputChange, dragAmount: Offset ->
                if (abs(dragAmount.y) > abs(dragAmount.x)) {
                    //This is a real swipe down the slide
                    change.consumeAllChanges()
                    onVerticalDrag(change, dragAmount.y)

                }
            }

            if (model.inhibitGesture) {
                onDragCancel()
            } else {
                if (drag != null) {
                    onDragStart.invoke(drag.position)
                    if (
                        verticalDrag(drag.id) {
                            onVerticalDrag(it, it.positionChange().y)
                        }
                    ) {
                        onDragEnd()
                    } else {
                        onDragCancel()
                    }
                } else {
                    //click.
                    if (!model.inhibitGesture)
                        onClick(down.position)
                }
            }

        }
    }
}

现在在寻呼机中,它正在使用 .horizontalDrag。如果您实际上可以纯水平或纯垂直拖动,这很好,但您不能,并且在内部控件上旨在垂直滑动通常会有一点点水平运动,导致寻呼机拦截它。所以在寻呼机中,我也不得不复制和修改一些代码来制作我自己的 .horizo​​ntalDrag;

suspend fun AwaitPointerEventScope.horizontalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
): Boolean = drag(
    pointerId = pointerId,
    onDrag = onDrag,
    motionFromChange = { if (abs(it.positionChangeIgnoreConsumed().x) > 10) it.positionChangeIgnoreConsumed().x else 0f },
    motionConsumed = { it.positionChangeConsumed() }
)

仅当移动的水平分量大于 10px 时才会触发。

最后,由于水平滚动也可能有一些垂直元素可能会影响内部控件,在我的 onPagerScrollStartonPagerScrollFinished 回调中,我在模型中设置并清除了一个标志,inhibitGesture 这会导致内部控件忽略在寻呼机滚动过程中碰巧遇到的手势。