You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HorizontalPager与可滑动Card手势冲突:仅边界超滑触发卡片滑动

问题描述

我在可滑动Card内部嵌套了带图片的HorizontalPager,界面效果如下:

  • 首页:卡片内包含图片轮播组件
  • 卡片滑动状态:卡片处于左右滑动的拖动过程中

当前核心问题:点击HorizontalPager时,它会拦截所有拖拽手势。我需要实现仅在超滑(overscroll)时触发卡片滑动:

  • 当Pager处于第一张图片时,允许向左滑动卡片
  • 当Pager处于最后一张图片时,允许向右滑动卡片
相关代码

带滑动修饰符的Card代码

Card(
        modifier = modifier
            .swipableCard(
                state = swipeState,
                onSwiped = {
                    when (it) {
                        Direction.Left -> onLeftSwipe()
                        Direction.Right -> onRightSwipe()
                        else -> {}
                    }
                },
                blockedDirections = listOf(Direction.Up, Direction.Down)
            ),
        colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colors.surface),
        shape = RoundedCornerShape(16.dp)
    ) {
        Box(modifier = Modifier.fillMaxSize()) {
            PostCardInfoUi(
                post = post,
                onDescriptionClicked = onDescriptionClicked,
                onOwnProfileClicked = onOwnProfileClicked,
                onOtherProfileClicked = onOtherProfileClicked,
                pagerState = pagerState
            )
            ReactionOverlay(
                modifier = Modifier.fillMaxSize(),
                isLike = swipeState.offset.value.x > 0,
                alpha = (absoluteOffsetDp.value / 100).coerceAtLeast(0f)
            )
        }
    }

滑动修饰符及相关代码

fun Modifier.swipableCard(
    state: SwipeableCardState,
    onSwiped: (Direction) -> Unit,
    onSwipeCancel: () -> Unit = {},
    blockedDirections: List<Direction> = listOf(Direction.Up, Direction.Down),
) = pointerInput(key1 = pagerPage, key2 = pagerCoordsRect) {
    coroutineScope {
        detectDragGestures(
            onDragCancel = {
                launch {
                    state.reset()
                    onSwipeCancel()
                }
            },
            onDrag = { change, dragAmount ->
                launch {
                    Log.i("SwipableCard", "Drag amount: $dragAmount")
                    val original = state.offset.targetValue
                    val summed = original + dragAmount
                    val newValue = Offset(
                        x = summed.x.coerceIn(-state.maxWidth, state.maxWidth),
                        y = summed.y.coerceIn(-state.maxHeight, state.maxHeight)
                    )
                    if (change.positionChange() != Offset.Zero) change.consume()
                    state.drag(newValue.x, newValue.y)
                }
            },
            onDragEnd = {
                launch {
                    val coercedOffset = state.offset.targetValue
                        .coerceIn(
                            blockedDirections,
                            maxHeight = state.maxHeight,
                            maxWidth = state.maxWidth
                        )

                    if (hasNotTravelledEnough(state, coercedOffset)) {
                        state.reset()
                        onSwipeCancel()
                    } else {
                        val horizontalTravel = abs(state.offset.targetValue.x)
                        val verticalTravel = abs(state.offset.targetValue.y)

                        if (horizontalTravel > verticalTravel) {
                            if (state.offset.targetValue.x > 0) {
                                state.swipe(Direction.Right)
                                onSwiped(Direction.Right)
                            } else {
                                state.swipe(Direction.Left)
                                onSwiped(Direction.Left)
                            }
                        } else {
                            if (state.offset.targetValue.y < 0) {
                                state.swipe(Direction.Up)
                                onSwiped(Direction.Up)
                            } else {
                                state.swipe(Direction.Down)
                                onSwiped(Direction.Down)
                            }
                        }
                    }
                }
            },

            )
    }
}.graphicsLayer {
    translationX = state.offset.value.x
    translationY = state.offset.value.y
    rotationZ = (state.offset.value.x / 60).coerceIn(-40f, 40f)
}


private fun Offset.coerceIn(
    blockedDirections: List<Direction>,
    maxHeight: Float,
    maxWidth: Float,
): Offset {
    return copy(
        x = x.coerceIn(
            if (blockedDirections.contains(Direction.Left)) {
                0f
            } else {
                -maxWidth
            },
            if (blockedDirections.contains(Direction.Right)) {
                0f
            } else {
                maxWidth
            }
        ),
        y = y.coerceIn(
            if (blockedDirections.contains(Direction.Up)) {
                0f
            } else {
                -maxHeight
            },
            if (blockedDirections.contains(Direction.Down)) {
                0f
            } else {
                maxHeight
            }
        )
    )
}

private fun hasNotTravelledEnough(
    state: SwipeableCardState,
    offset: Offset,
): Boolean {
    return abs(offset.x) < state.maxWidth / 4 &&
            abs(offset.y) < state.maxHeight / 4
}

enum class Direction {
    Left, Right, Up, Down
}

@Composable
fun rememberSwipeableCardState(): SwipeableCardState {
    val screenWidth = with(LocalDensity.current) {
        LocalConfiguration.current.screenWidthDp.dp.toPx()
    }
    val screenHeight = with(LocalDensity.current) {
        LocalConfiguration.current.screenHeightDp.dp.toPx()
    }
    return remember {
        SwipeableCardState(screenWidth, screenHeight)
    }
}


class SwipeableCardState(
    internal val maxWidth: Float,
    internal val maxHeight: Float,
) {
    val offset = Animatable(offset(0f, 0f), Offset.VectorConverter)

    /**
     * The [Direction] the card was swiped at.
     *
     * Null value means the card has not been swiped fully yet.
     */
    var swipedDirection: Direction? by mutableStateOf(null)
        private set

    internal suspend fun reset() {
        offset.animateTo(offset(0f, 0f), tween(400))
    }

    suspend fun swipe(direction: Direction, animationSpec: AnimationSpec<Offset> = tween(400)) {
        val endX = maxWidth * 1.5f
        val endY = maxHeight
        when (direction) {
            Direction.Left -> offset.animateTo(offset(x = -endX), animationSpec)
            Direction.Right -> offset.animateTo(offset(x = endX), animationSpec)
            Direction.Up -> offset.animateTo(offset(y = -endY), animationSpec)
            Direction.Down -> offset.animateTo(offset(y = endY), animationSpec)
        }
        this.swipedDirection = direction
    }

    private fun offset(x: Float = offset.value.x, y: Float = offset.value.y): Offset {
        return Offset(x, y)
    }

    internal suspend fun drag(x: Float, y: Float) {
        offset.animateTo(offset(x, y))
    }
}
已尝试的方案

我尝试自定义detectDragGestures扩展函数,传入HorizontalPager的坐标和当前页面状态(首页、中间页、末页),但未成功:

  • 如果消费awaitFirstDown()指针输入,Pager将无法接收输入
  • 如果等待touchSlop后根据positionChange()方向和坐标决定滑动Pager还是卡片,HorizontalPager会先消费awaitFirstDown()指针,导致卡片无法触发滑动

内容的提问来源于stack exchange,提问作者JackSaf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 12:47:09