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

Jetpack Compose拖拽左侧手柄调整Composable水平尺寸异常的解决办法

问题:拖拽左侧手柄时错误移动右侧手柄,如何正确实现Clip的左侧拖拽调整?

我尝试创建一个可调整尺寸的Composable,用户拖拽其手柄时可进行水平方向的尺寸调整。

定义了Clip类存储起始点和宽度:

data class Clip(
    var start: Float,
    var width: Float,
    var isDeleted: Boolean = false,
)

创建了DraggableClip组件,拖拽右侧手柄调整尺寸功能正常,但拖拽左侧手柄时,移动的却是右侧手柄。如何实现左侧手柄拖拽时正确移动并调整Clip的宽度,且不影响右侧手柄的位置?

DraggableClip代码

@Composable
fun DraggableClip(
    clip: Clip,
    maxWidth: Float,
    onDelete: () -> Unit,
    onDrag: (Float) -> Unit,
    clips: List<Clip>,
) {
    var offsetX by remember { mutableStateOf(clip.start) }

    var clipWidth by remember { mutableStateOf(clip.width) }
    var leftHandlePosition by remember { mutableStateOf(0f) }
    var rightHandlePosition by remember { mutableStateOf(clipWidth) }

    Row(
        modifier = Modifier
            .offset { IntOffset(clip.start.roundToInt(), 0) }
            .height(100.dp)
    ) {
        // Left Handle
        Box(
            modifier = Modifier
                .width(10.dp)
                .fillMaxHeight()
                .background(Color.Gray)
                .let {
                    if (clip.isDeleted) it else {
                        it.draggable(
                            orientation = Orientation.Horizontal,
                            state = rememberDraggableState { delta ->
                                val proposedLeftPosition = leftHandlePosition + delta
                                val proposedWidth = clipWidth - delta

                                leftHandlePosition = proposedLeftPosition.coerceIn(0f, rightHandlePosition - 10f)
                                clipWidth = proposedWidth
                            }
                        )
                    }
                }
        )

        // Main Draggable Clip
        Box(
            modifier = Modifier
                .width(clipWidth.dp)
                .fillMaxHeight()
                .background(if (clip.isDeleted) Color.White else Color.Red)
                .let {
                    if (clip.isDeleted) it else it.draggable(
                        orientation = Orientation.Horizontal,
                        state = rememberDraggableState { delta ->
                            val proposedOffsetX = offsetX + delta

                            offsetX = proposedOffsetX.coerceIn(0f, maxWidth - clip.width)
                            onDrag(offsetX)
                        },
                    )
                }
        )

        // Right Handle
        Box(
            modifier = Modifier
                .width(10.dp)
                .fillMaxHeight()
                .background(Color.Gray)
                .let {
                    if (clip.isDeleted) it else {
                        it.draggable(
                            orientation = Orientation.Horizontal,
                            state = rememberDraggableState { delta ->
                                val proposedRightPosition = rightHandlePosition + delta

                                rightHandlePosition = proposedRightPosition.coerceIn(leftHandlePosition + 10f, maxWidth)
                                clipWidth = rightHandlePosition - leftHandlePosition
                            }
                        )
                    }
                }
        )
    }
}

测试组件代码

@Composable
fun DraggableClipRow() {
    val clip1 = Clip(0f, 150f)
    val clips = listOf(clip1)

    Row(
        modifier = Modifier
            .padding(20.dp)
            .fillMaxSize(),
        horizontalArrangement = Arrangement.Start
    ) {
        DraggableClip(
            clip = clip1,
            maxWidth = 500f,
            onDelete = {},
            onDrag = {},
            clips = clips
        )
    }
}

解决方案

问题核心在于:左侧拖拽时未同步更新Clip的起始位置,且状态变量与原始数据绑定逻辑混乱,导致视觉上出现右侧手柄移动的错觉。以下是修复后的代码:

修改后的DraggableClip

@Composable
fun DraggableClip(
    clip: Clip,
    maxWidth: Float,
    onDelete: () -> Unit,
    onDrag: (Clip) -> Unit,
    clips: List<Clip>,
) {
    var offsetX by remember { mutableStateOf(clip.start) }
    var clipWidth by remember { mutableStateOf(clip.width) }

    Row(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .height(100.dp)
    ) {
        // Left Handle
        Box(
            modifier = Modifier
                .width(10.dp)
                .fillMaxHeight()
                .background(Color.Gray)
                .let {
                    if (clip.isDeleted) it else {
                        it.draggable(
                            orientation = Orientation.Horizontal,
                            state = rememberDraggableState { delta ->
                                val newOffset = offsetX + delta
                                val newWidth = clipWidth - delta

                                // 限制边界:起始位置不小于0,宽度不小于最小尺寸,整体不超过最大宽度
                                val clampedOffset = newOffset.coerceIn(0f, maxWidth - 10f)
                                val clampedWidth = newWidth.coerceIn(10f, maxWidth - clampedOffset)

                                offsetX = clampedOffset
                                clipWidth = clampedWidth

                                // 同步更新原始Clip数据并回调
                                clip.start = offsetX
                                clip.width = clipWidth
                                onDrag(clip)
                            }
                        )
                    }
                }
        )

        // Main Draggable Clip
        Box(
            modifier = Modifier
                .width(clipWidth.dp)
                .fillMaxHeight()
                .background(if (clip.isDeleted) Color.White else Color.Red)
                .let {
                    if (clip.isDeleted) it else it.draggable(
                        orientation = Orientation.Horizontal,
                        state = rememberDraggableState { delta ->
                            val newOffset = offsetX + delta
                            // 用当前宽度计算拖拽边界
                            offsetX = newOffset.coerceIn(0f, maxWidth - clipWidth)

                            clip.start = offsetX
                            onDrag(clip)
                        },
                    )
                }
        )

        // Right Handle
        Box(
            modifier = Modifier
                .width(10.dp)
                .fillMaxHeight()
                .background(Color.Gray)
                .let {
                    if (clip.isDeleted) it else {
                        it.draggable(
                            orientation = Orientation.Horizontal,
                            state = rememberDraggableState { delta ->
                                val newWidth = clipWidth + delta
                                // 限制宽度边界
                                clipWidth = newWidth.coerceIn(10f, maxWidth - offsetX)

                                clip.width = clipWidth
                                onDrag(clip)
                            }
                        )
                    }
                }
        )
    }
}

修改后的测试组件

@Composable
fun DraggableClipRow() {
    val clip1 = remember { mutableStateOf(Clip(0f, 150f)) }
    val clips = listOf(clip1.value)

    Row(
        modifier = Modifier
            .padding(20.dp)
            .fillMaxSize(),
        horizontalArrangement = Arrangement.Start
    ) {
        DraggableClip(
            clip = clip1.value,
            maxWidth = 500f,
            onDelete = {},
            onDrag = { updatedClip ->
                clip1.value = updatedClip.copy()
            },
            clips = clips
        )
    }
}

关键修改说明

  1. 移除冗余的leftHandlePosition和rightHandlePosition,直接用offsetX(对应Clip的start)和clipWidth控制位置与尺寸,简化状态逻辑。
  2. 左侧拖拽时同步更新offsetX和clipWidth,确保左侧移动时整体位置同步变化,右侧手柄自然保持正确位置。
  3. 主区域拖拽时使用当前clipWidth计算边界,避免依赖旧数据导致错误。
  4. 修改onDrag回调传递完整Clip对象,确保外部数据与组件状态同步。
  5. 测试组件用mutableStateOf包裹Clip,保证数据变化能触发UI重组。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 07:30:56