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

Jetpack Compose拖拽Composable:图片拖拽后位置更新异常

Jetpack Compose图片拖拽释放后位置更新异常问题

我参考常见场景实现,尝试在Jetpack Compose中实现图片拖拽移动功能,当前是用Fragment承载Compose内容的遗留应用,但这和问题无关。

代码实现

class DAndDFragment : Fragment() {
    private val viewModel by viewModels<DAndDViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)

            setContent {
                val pip by viewModel.pip.collectAsState()

                AppTheme {                    
                    DragAndDrop(modifier = Modifier.fillMaxSize()) {
                        Dragged(
                            modifier = Modifier.offset(
                                pip.pos.x.dp, pip.pos.y.dp
                            ),
                            onDropped = { position ->
                                viewModel.pos(position.x, position.y)
                            }
                        ) {
                            Box(modifier = Modifier.size(pip.size.dp)) {
                                Image(
                                    painter = rememberAsyncImagePainter(pip.image),
                                    contentDescription = null,
                                    contentScale = ContentScale.Crop,
                                    modifier = Modifier
                                        .size(pip.size.dp)
                                        .clip(appRoundedCornerShape())
                                )
                            }
                        }
                    }
                }
            }
        }
    }

    @Composable
    fun Dragged(
        modifier: Modifier = Modifier,
        onDropped: (Offset) -> Unit,
        content: @Composable (Boolean) -> Unit
    ) {
        var currentPosition by remember { mutableStateOf(Offset.Zero) }
        val currentState = LocalDragTargetInfo.current
        Box(modifier = modifier
            .onGloballyPositioned {
                currentPosition = it.localToWindow(Offset.Zero)
            }
            .pointerInput(Unit) {
                detectDragGesturesAfterLongPress(onDragStart = {
                    currentState.isDragging = true
                    currentState.position = currentPosition + it
                    currentState.draggableComposable = content
                }, onDrag = { change, dragAmount ->
                    change.consume()
                    currentState.offset += dragAmount
                }, onDragEnd = {
                    currentState.isDragging = false
                    currentState.position += currentState.offset
                    onDropped(currentState.position)
                    currentState.offset = Offset.Zero
                }, onDragCancel = {
                    currentState.offset = Offset.Zero
                    currentState.isDragging = false
                })
            }) {
            content(currentState.isDragging)
        }
    }
}

internal class DragTargetInfo {
    var isDragging: Boolean by mutableStateOf(false)
    var position: Offset by mutableStateOf(Offset(0f, 0f))
    var offset: Offset by mutableStateOf(Offset.Zero)
    var draggableComposable by mutableStateOf<(@Composable (Boolean) -> Unit)>({ })
}

internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() }

@Composable
fun DragAndDrop(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    val state = remember { DragTargetInfo() }
    CompositionLocalProvider(
        LocalDragTargetInfo provides state
    ) {
        Box(modifier = modifier.fillMaxSize())
        {
            content()
            if (state.isDragging) {
                var targetSize by remember {
                    mutableStateOf(IntSize.Zero)
                }
                Box(modifier = Modifier
                    .graphicsLayer {
                        val offset = state.position + state.offset
                        alpha = if (targetSize == IntSize.Zero) 0f else .5f
                        translationX = offset.x.minus(targetSize.width / 2)
                        translationY = offset.y.minus(targetSize.height / 2)
                    }
                    .onGloballyPositioned {
                        targetSize = it.size
                    }
                ) {
                    state.draggableComposable(true)
                }
            }
        }
    }
}

问题描述

拖拽过程一切正常,但释放拖拽的图片时,应用触发onDragEnd回调后,通过currentState.position += currentState.offset计算的最终位置无法正确更新图片的实际位置,而同样的计算方式在DragAndDrop组件中显示拖拽中的图片时是正常的。请问这是什么原因?该如何解决?


问题原因与解决方案

原因分析

  1. 坐标体系不匹配:currentPosition是通过localToWindow获取的窗口坐标系位置,但图片最终用Modifier.offset(x.dp, y.dp)设置的是父布局本地坐标系的偏移,两者基准点不同(窗口坐标系以屏幕左上角为原点,本地坐标系以父布局左上角为原点),直接传递窗口坐标会导致位置偏差。
  2. 预览与最终位置逻辑不一致:拖拽预览时通过minus(targetSize.width/2)让元素居中显示,但最终位置计算未考虑这个偏移,导致释放后图片位置和预览位置不匹配。

解决方案

需要将窗口坐标转换为父布局本地坐标,并对齐预览逻辑的居中偏移,修改后的Dragged函数如下:

@Composable
fun Dragged(
    modifier: Modifier = Modifier,
    onDropped: (Offset) -> Unit,
    content: @Composable (Boolean) -> Unit
) {
    val parentCoordinates = LocalLayoutCoordinates.current
    var currentPosition by remember { mutableStateOf(Offset.Zero) }
    val currentState = LocalDragTargetInfo.current
    var elementSize by remember { mutableStateOf(IntSize.Zero) }
    val density = LocalDensity.current

    Box(modifier = modifier
        .onGloballyPositioned {
            currentPosition = it.localToWindow(Offset.Zero)
            elementSize = it.size
        }
        .pointerInput(Unit) {
            detectDragGesturesAfterLongPress(onDragStart = { pressOffset ->
                currentState.isDragging = true
                currentState.position = currentPosition + pressOffset
                currentState.draggableComposable = content
            }, onDrag = { change, dragAmount ->
                change.consume()
                currentState.offset += dragAmount
            }, onDragEnd = {
                currentState.isDragging = false
                // 计算最终窗口位置
                val finalWindowPosition = currentState.position + currentState.offset
                // 转换为父布局本地坐标系
                val finalLocalPosition = parentCoordinates.windowToLocal(finalWindowPosition)
                // 减去元素半尺寸,和预览逻辑对齐
                val adjustedPosition = finalLocalPosition - Offset(
                    elementSize.width / 2f,
                    elementSize.height / 2f
                )
                // 转换为dp单位,匹配ViewModel存储的格式
                val dpPosition = with(density) {
                    Offset(adjustedPosition.x.toDp().value, adjustedPosition.y.toDp().value)
                }
                onDropped(dpPosition)
                
                currentState.offset = Offset.Zero
            }, onDragCancel = {
                currentState.offset = Offset.Zero
                currentState.isDragging = false
            })
    ) {
        content(currentState.isDragging)
    }
}

关键修改说明

  • 使用LocalLayoutCoordinates.current将窗口坐标转换为父布局本地坐标,确保和Modifier.offset的坐标系一致。
  • 减去元素半尺寸,让最终位置和拖拽预览的视觉位置对齐,避免释放后图片跳变。
  • 将像素坐标转换为dp单位,匹配你ViewModel中存储的位置格式,确保Modifier.offset(x.dp, y.dp)能正确生效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 16:17:53