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
相关产品推荐
相关产品推荐

