Android Jetpack Compose:如何仅捕获Box组件内或相机遮罩中显示的图像
解决Jetpack Compose中Box区域图像捕获的两个场景
嘿,你已经搞定了最关键的两步——拿到目标Bitmap和Box的屏幕坐标,剩下的就是把这两者结合起来做精准裁剪啦!我分两个场景给你详细说:
场景1:捕获Box组件内显示的静态图像
你已经通过onGloballyPositioned拿到了Box(也就是你的Image组件)的屏幕坐标rect,接下来要把这个屏幕区域映射到原始Bitmap的像素位置,因为Image组件可能会对图像做缩放、居中处理(比如默认的ContentScale.Fit)。
具体步骤&代码:
- 先准备好你的原始Bitmap和Box的坐标:
val originalBitmap: Bitmap = ... // 你的原始图像Bitmap val boxRect: Rect = ... // 从boundsInRoot获取的Box屏幕坐标 val imageCoordinates: LayoutCoordinates = ... // onGloballyPositioned回调里的coordinates
- 计算图像在Image组件内的缩放比例和偏移量(适配
ContentScale.Fit的情况):
// 原始Bitmap的尺寸 val bitmapWidth = originalBitmap.width.toFloat() val bitmapHeight = originalBitmap.height.toFloat() // Image组件的显示尺寸 val imageViewWidth = imageCoordinates.size.width.toFloat() val imageViewHeight = imageCoordinates.size.height.toFloat() // 计算缩放比例(保持图像比例,取最小的缩放值) val scaleX = imageViewWidth / bitmapWidth val scaleY = imageViewHeight / bitmapHeight val actualScale = min(scaleX, scaleY) // 计算图像在Image组件内的居中偏移量 val offsetX = (imageViewWidth - bitmapWidth * actualScale) / 2f val offsetY = (imageViewHeight - bitmapHeight * actualScale) / 2f
- 将Box的屏幕区域转换为Bitmap上的像素区域,然后裁剪:
// 因为Box就是Image组件本身,所以Box的区域对应Image内的全区域,转换为Bitmap的坐标 val bitmapLeft = (offsetX / actualScale).toInt() val bitmapTop = (offsetY / actualScale).toInt() val cropWidth = (imageViewWidth / actualScale).toInt() val cropHeight = (imageViewHeight / actualScale).toInt() // 做边界检查,避免超出Bitmap范围报错 val safeLeft = max(0, bitmapLeft) val safeTop = max(0, bitmapTop) val safeWidth = min(originalBitmap.width - safeLeft, cropWidth) val safeHeight = min(originalBitmap.height - safeTop, cropHeight) // 最终裁剪出的Bitmap val croppedBitmap = Bitmap.createBitmap(originalBitmap, safeLeft, safeTop, safeWidth, safeHeight)
场景2:相机遮罩中捕获Box内的图像
这个场景要额外处理相机预览的方向和缩放问题,因为相机捕获的Bitmap方向可能和屏幕显示不一致,而且预览画面会适配屏幕尺寸做缩放。
具体步骤&代码:
- 准备好相机捕获的Bitmap、遮罩Box的屏幕坐标,以及相机预览组件的尺寸和方向:
val cameraCapturedBitmap: Bitmap = ... // 相机捕获的原始Bitmap val maskBoxRect: Rect = ... // 遮罩Box的屏幕坐标 val previewViewSize: Size = ... // 屏幕上相机预览组件的尺寸(比如PreviewView的宽高) val cameraOrientation: Int = ... // 相机的旋转角度(比如竖屏时是90度) val previewViewTopLeft: Offset = ... // 预览组件在屏幕上的左上角坐标
- 先把相机Bitmap旋转到和屏幕一致的方向:
val rotatedBitmap = if (cameraOrientation != 0) { val matrix = Matrix() matrix.postRotate(cameraOrientation.toFloat()) Bitmap.createBitmap( cameraCapturedBitmap, 0, 0, cameraCapturedBitmap.width, cameraCapturedBitmap.height, matrix, true ) } else { cameraCapturedBitmap }
- 计算预览画面到Bitmap的缩放比例:
val rotatedBitmapWidth = rotatedBitmap.width.toFloat() val rotatedBitmapHeight = rotatedBitmap.height.toFloat() // 缩放比例:Bitmap尺寸 / 预览组件显示尺寸 val scaleX = rotatedBitmapWidth / previewViewSize.width val scaleY = rotatedBitmapHeight / previewViewSize.height
- 将遮罩Box的屏幕坐标转换为相对于预览组件的坐标,再映射到Bitmap:
// 遮罩Box相对于预览组件的坐标 val maskRelativeLeft = maskBoxRect.left - previewViewTopLeft.x val maskRelativeTop = maskBoxRect.top - previewViewTopLeft.y val maskRelativeWidth = maskBoxRect.width val maskRelativeHeight = maskBoxRect.height // 转换为Bitmap上的像素坐标 val bitmapLeft = (maskRelativeLeft * scaleX).toInt() val bitmapTop = (maskRelativeTop * scaleY).toInt() val cropWidth = (maskRelativeWidth * scaleX).toInt() val cropHeight = (maskRelativeHeight * scaleY).toInt() // 边界检查 val safeLeft = max(0, bitmapLeft) val safeTop = max(0, bitmapTop) val safeWidth = min(rotatedBitmap.width - safeLeft, cropWidth) val safeHeight = min(rotatedBitmap.height - safeTop, cropHeight) // 最终裁剪的Bitmap val croppedCameraBitmap = Bitmap.createBitmap(rotatedBitmap, safeLeft, safeTop, safeWidth, safeHeight)
额外注意事项:
- 记得在使用完Bitmap后调用
recycle(),避免内存泄漏。 - 如果你的Image组件用了其他
ContentScale(比如Fill),需要调整缩放比例的计算逻辑。 - 相机场景中,如果你用的是CameraX,也可以通过
ImageAnalysis直接在分析阶段裁剪,效率更高。
内容的提问来源于stack exchange,提问作者Kunal Kalwar




