如何在Android中使用CameraX拍摄相机屏幕指定区域的照片?
解决CameraX拍摄指定区域的问题
问题背景
你需要拍摄相机预览界面中camera_frame(250dp×250dp)框选的区域,之前尝试计算裁剪和第三方库都未成功,核心问题是预览界面坐标与相机实际输出图片的坐标不匹配,以及缩放、屏幕旋转带来的转换误差。
下面提供两种可靠的解决方案:
方案一:拍摄后精准裁剪(适配所有场景)
先拍摄完整照片,再根据预览框位置计算出需要裁剪的区域,彻底解决坐标映射问题。
步骤1:修改photoCapture()方法
替换你原有的photoCapture()方法,整合坐标转换与裁剪逻辑:
private fun photoCapture() { imageCapture ?: return val photoFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), SimpleDateFormat("yyyyMMddHHmmSS", Locale.US).format(System.currentTimeMillis()) + ".jpeg") val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() imageCapture!!.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(exc: ImageCaptureException) { try { photoFile.delete() } catch (e: Exception) { e.printStackTrace() } Toast.makeText(this@CameraActivity, "拍摄失败!", Toast.LENGTH_LONG).show() } override fun onImageSaved(output: ImageCapture.OutputFileResults) { // 1. 获取预览框的屏幕坐标 val frameLocation = IntArray(2) binding.cameraFrame.getLocationOnScreen(frameLocation) val frameLeft = frameLocation[0] val frameTop = frameLocation[1] val frameWidth = binding.cameraFrame.width val frameHeight = binding.cameraFrame.height // 2. 获取预览到图片的转换矩阵,解决缩放/比例差异 val previewView = binding.previewView val matrix = Matrix() previewView.getPreviewMatrix(matrix) matrix.invert(matrix) // 反转矩阵,将屏幕坐标转成图片坐标 // 3. 计算图片上对应的裁剪区域 val frameRect = RectF(frameLeft.toFloat(), frameTop.toFloat(), (frameLeft + frameWidth).toFloat(), (frameTop + frameHeight).toFloat()) matrix.mapRect(frameRect) // 4. 读取并裁剪图片 val originalBitmap = BitmapFactory.decodeFile(photoFile.absolutePath) // 确保裁剪坐标不超出图片范围 val cropLeft = frameRect.left.toInt().coerceAtLeast(0) val cropTop = frameRect.top.toInt().coerceAtLeast(0) val cropWidth = frameRect.width().toInt().coerceAtMost(originalBitmap.width - cropLeft) val cropHeight = frameRect.height().toInt().coerceAtMost(originalBitmap.height - cropTop) val croppedBitmap = Bitmap.createBitmap(originalBitmap, cropLeft, cropTop, cropWidth, cropHeight) originalBitmap.recycle() // 释放原图片内存 // 5. 保存裁剪后的图片 val croppedFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "cropped_${System.currentTimeMillis()}.jpeg") try { FileOutputStream(croppedFile).use { out -> croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) } croppedBitmap.recycle() photoFile.delete() // 删除原全图(可选) // 跳转到预览界面 CameraPreviewActivity.path = croppedFile.absolutePath startActivity(Intent(this@CameraActivity, CameraPreviewActivity::class.java)) } catch (e: Exception) { e.printStackTrace() Toast.makeText(this@CameraActivity, "裁剪失败!", Toast.LENGTH_LONG).show() } } } ) }
方案二:让CameraX直接拍摄指定区域(更高效)
通过设置ImageCapture的裁剪区域,让相机直接输出指定区域的照片,无需后续裁剪,性能更好。
步骤1:修改initializeCamera()方法
在相机初始化时计算传感器坐标系下的裁剪区域,并设置给ImageCapture:
private fun initializeCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() val preview = Preview.Builder() .build() .also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } // 等待PreviewView加载完成后计算裁剪区域 binding.previewView.post { // 1. 获取相机传感器信息 val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview) val cameraInfo = camera.cameraInfo val sensorSize = cameraInfo.sensorResolution val previewSize = binding.previewView.width to binding.previewView.height // 2. 获取预览框的屏幕坐标 val frameLocation = IntArray(2) binding.cameraFrame.getLocationOnScreen(frameLocation) val frameLeft = frameLocation[0] val frameTop = frameLocation[1] val frameWidth = binding.cameraFrame.width val frameHeight = binding.cameraFrame.height // 3. 计算屏幕到传感器的缩放比例 val scaleX = sensorSize.width / previewSize.first.toFloat() val scaleY = sensorSize.height / previewSize.second.toFloat() val scale = max(scaleX, scaleY) // 4. 根据屏幕方向转换坐标(传感器坐标系为横向) val frameCenterX = frameLeft + frameWidth / 2 val frameCenterY = frameTop + frameHeight / 2 val (sensorCenterX, sensorCenterY) = when (resources.configuration.orientation) { Configuration.ORIENTATION_PORTRAIT -> { val x = sensorSize.height - (frameCenterY * scale) val y = frameCenterX * scale x to y } else -> { val x = frameCenterX * scale val y = sensorSize.height - (frameCenterY * scale) x to y } } // 5. 构建传感器坐标系下的裁剪矩形 val cropWidth = (frameWidth * scale).toInt() val cropHeight = (frameHeight * scale).toInt() val cropRect = Rect( (sensorCenterX - cropWidth / 2).toInt(), (sensorCenterY - cropHeight / 2).toInt(), (sensorCenterX + cropWidth / 2).toInt(), (sensorCenterY + cropHeight / 2).toInt() ) // 6. 创建带裁剪的ImageCapture实例 imageCapture = ImageCapture.Builder() .setCropRect(cropRect) .build() // 重新绑定相机 cameraProvider.unbindAll() cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) } }, ContextCompat.getMainExecutor(this)) }
注意事项
- 部分设备可能不支持硬件裁剪,此时会自动降级为软件裁剪,效果一致但性能稍差。
- 屏幕旋转后需要重新计算裁剪区域,可在
onConfigurationChanged()中重新调用初始化逻辑。
关键优化点
- 坐标映射:通过PreviewView的转换矩阵解决预览与拍摄图片的比例差异,避免裁剪错位。
- 边界校验:强制裁剪坐标在图片范围内,防止崩溃。
- 内存管理:及时回收Bitmap,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Kaljal
相关产品推荐
相关产品推荐

