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

Android Compose Kotlin:如何像谷歌地图一样在图片上放置标记

解决Compose中图片缩放后标记坐标偏移问题

核心问题出在你直接使用点击事件返回的Offset(相对于Image组件的像素坐标)和dp转换来定位标记,没有考虑ContentScale.Fit导致的图片缩放和留白,以及不同屏幕的适配。正确的做法是保存相对于原图的比例坐标,而非绝对像素或dp,这样就能在所有设备上正确映射位置。

解决步骤

  • 获取图片的原始尺寸
  • 计算图片在Image组件中实际绘制的区域(缩放比例、偏移量)
  • 将点击坐标转换为相对于原图的比例(0~1之间的数值)
  • 保存比例坐标到数据库,而非绝对像素
  • 显示标记时,根据当前图片绘制区域,将比例转换为屏幕坐标

修改后的代码实现

1. 图片尺寸与绘制区域计算

在ImageScreen中添加变量记录图片原始尺寸、Image组件尺寸,以及图片实际绘制的区域:

@Composable
fun ImageScreen(navController: NavController) {
    val context = LocalContext.current
    val painter = painterResource(R.drawable.hair_picture)
    // 图片原始尺寸
    val originalImageSize = painter.intrinsicSize
    // Image组件的实际尺寸
    var imageComponentSize by remember { mutableStateOf(Size.Zero) }
    // 图片实际绘制的区域(缩放后的尺寸+偏移)
    var imageDrawArea by remember { mutableStateOf(Rect.Zero) }
    
    var xyCoordinates by remember { mutableStateOf(Offset.Zero) }
    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
    val scope = rememberCoroutineScope()
    var showBottomSheet by remember { mutableStateOf(false) }

    // 修改示例数据:保存相对于原图的比例(0~1),而非绝对像素
    val imgAnnotations = remember {
        mutableStateListOf<ImgAnnotation>()
            .apply {
                add(
                    ImgAnnotation(
                        uid = "45224",
                        coordinateX = 0.02f, // 原图宽度的2%
                        coordinateY = 0.02f, // 原图高度的2%
                        note = "Sample text 1"
                    )
                )
                add(
                    ImgAnnotation(
                        uid = "6454",
                        coordinateX = 0.1f,
                        coordinateY = 0.1f,
                        note = "Sample text 2"
                    )
                )
            }
    }

    var currentAnnotationSelected by remember { mutableStateOf(ImgAnnotation()) }
    var showAnnotation by remember { mutableStateOf(false) }

    // 计算图片实际绘制区域
    LaunchedEffect(imageComponentSize, originalImageSize) {
        if (imageComponentSize.width == 0f || imageComponentSize.height == 0f) return@LaunchedEffect
        
        // 计算缩放比例(ContentScale.Fit的逻辑)
        val scale = minOf(
            imageComponentSize.width / originalImageSize.width,
            imageComponentSize.height / originalImageSize.height
        )
        // 缩放后的图片尺寸
        val scaledImageWidth = originalImageSize.width * scale
        val scaledImageHeight = originalImageSize.height * scale
        // 图片在Image组件中的偏移(居中显示的留白)
        val offsetX = (imageComponentSize.width - scaledImageWidth) / 2f
        val offsetY = (imageComponentSize.height - scaledImageHeight) / 2f
        
        imageDrawArea = Rect(offsetX, offsetY, offsetX + scaledImageWidth, offsetY + scaledImageHeight)
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Bottom,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.fillMaxSize()
        ) {
            Image(
                painter = painter,
                contentDescription = "Record image",
                contentScale = ContentScale.Fit,
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .onSizeChanged { size ->
                        // 记录Image组件的实际尺寸
                        imageComponentSize = Size(size.width.toFloat(), size.height.toFloat())
                    }
                    .pointerInput(Unit) {
                        detectTapGestures(
                            onPress = { offset ->
                                // 将点击坐标转换为相对于原图的比例
                                if (imageDrawArea.isEmpty) return@detectTapGestures
                                
                                // 先计算点击位置相对于图片绘制区域的坐标
                                val relativeX = offset.x - imageDrawArea.left
                                val relativeY = offset.y - imageDrawArea.top
                                
                                // 再转换为相对于原图的比例
                                val xRatio = relativeX / imageDrawArea.width
                                val yRatio = relativeY / imageDrawArea.height
                                
                                // 保存比例坐标(后续存数据库)
                                xyCoordinates = Offset(xRatio, yRatio)
                                showBottomSheet = true
                            }
                        )
                    }
            )

            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier.fillMaxSize()
            ) {
                imgAnnotations.forEach { item ->
                    // 将比例坐标转换为当前屏幕的偏移量
                    val offsetX = imageDrawArea.left + (item.coordinateX ?: 0f) * imageDrawArea.width
                    val offsetY = imageDrawArea.top + (item.coordinateY ?: 0f) * imageDrawArea.height
                    
                    MakeShape(
                        modifier = Modifier
                            .offset { IntOffset(offsetX.toInt(), offsetY.toInt()) }
                            .clickable {
                                currentAnnotationSelected = item
                                showAnnotation = true
                                showBottomSheet = true
                            },
                        shape = CircleShape,
                        size = 20.dp,
                        bg = Color.Yellow
                    )
                }
            }

            // 底部弹窗部分保持不变
            if (showBottomSheet) {
                ModalBottomSheet(
                    onDismissRequest = {
                        showBottomSheet = false
                        showAnnotation = false
                        currentAnnotationSelected = ImgAnnotation()
                    },
                    sheetState = sheetState,
                    windowInsets = WindowInsets(0, 0, 0, 0)
                ) {
                    IconButton(
                        onClick = {
                            scope.launch { sheetState.hide() }.invokeOnCompletion {
                                if (!sheetState.isVisible) {
                                    showBottomSheet = false
                                    showAnnotation = false
                                    currentAnnotationSelected = ImgAnnotation()
                                }
                            }
                        },
                        modifier = Modifier.align(Alignment.End)
                    ) {
                        Icon(
                            painterResource(R.drawable.close_icon),
                            contentDescription = "Close icon",
                            modifier = Modifier.height(18.dp)
                        )
                    }

                    AnnotationNote(
                        xy = xyCoordinates,
                        annotationData = currentAnnotationSelected,
                        show = showAnnotation
                    )

                    Spacer(modifier = Modifier.height(16.dp))
                }
            }
        }
    }
}

2. 保存坐标到数据库

在AnnotationNote的保存按钮点击事件中,直接保存xyCoordinates的x和y(0~1的比例值):

ActionButton(
    onClick = {
        // 保存比例坐标到数据库:xyCoordinates.x 和 xyCoordinates.y
        // 示例:
        // val newAnnotation = ImgAnnotation(
        //     uid = UUID.randomUUID().toString(),
        //     coordinateX = xyCoordinates.x.toFloat(),
        //     coordinateY = xyCoordinates.y.toFloat(),
        //     note = annotationNote
        // )
        // imgAnnotations.add(newAnnotation)
        // 关闭弹窗
        scope.launch { sheetState.hide() }
    },
    contentColor = Color.Black,
    disabledContentColor = Color.Black,
    text = stringResource(R.string.save_btn),
)

关键说明

  • 比例坐标的优势:不管图片在什么屏幕上缩放、留白,比例坐标都能精准映射到原图的对应位置,彻底解决不同设备的适配问题。
  • 绘制区域计算:通过ContentScale.Fit的缩放逻辑,计算出图片实际显示的区域,确保点击坐标转换的准确性。
  • 偏移量转换:显示标记时,将比例坐标转换为当前屏幕的像素偏移,使用offset { IntOffset(...) }而非dp,避免单位转换错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 19:55:56