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

如何让Android相机拍摄尺寸与PreviewView预览区域一致

解决CameraX拍摄图片与PreviewView预览区域尺寸一致的问题

问题说明

拍摄得到的图片尺寸及比例与相机预览界面显示的不一致,需要拍摄出与PreviewView预览区域(非全屏的约束布局区域)尺寸完全匹配的图片。当前拍摄的图片为全屏尺寸,布局代码和拍照代码如下:

布局代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black">

    <androidx.camera.view.PreviewView
        android:id="@+id/preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="120dp"
        app:layout_constraintBottom_toTopOf="@id/bottom"
        app:layout_constraintEnd_toStartOf="@id/right"
        app:layout_constraintStart_toEndOf="@id/left"
        app:layout_constraintTop_toBottomOf="@id/top" />

    <ImageView
        android:id="@+id/photo_preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="120dp"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="@id/bottom"
        app:layout_constraintEnd_toStartOf="@id/right"
        app:layout_constraintStart_toEndOf="@id/left"
        app:layout_constraintTop_toBottomOf="@id/top" />

    <ImageView
        android:id="@+id/top"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/camera"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:tint="@color/white" />

    <ImageView
        android:id="@+id/left"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:tint="@color/white" />

    <ImageView
        android:id="@+id/right"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:tint="@color/white" />

    <ImageView
        android:id="@+id/bottom"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:tint="@color/white" />

</androidx.constraintlayout.widget.ConstraintLayout>

拍照代码

private fun takePhoto() {
    imageCapture.takePicture(ContextCompat.getMainExecutor(this), object :
        ImageCapture.OnImageCapturedCallback() {
        override fun onCaptureSuccess(image: ImageProxy) {
            val buffer = image.planes[0].buffer
            val bytes = ByteArray(buffer.remaining())
            buffer.get(bytes)

            val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
            
            // how to resize??
            
            binding.photoPreview.isVisible = true
            binding.photoPreview.post {
                binding.photoPreview.setImageBitmap(bitmap)
            }

            image.close()
        }

        override fun onError(exception: ImageCaptureException) {
            Log.e(TAG, "Photo capture failed: ${exception.message}", exception)
        }
    })
}

解决步骤

1. 处理图片旋转,匹配预览方向

CameraX输出的ImageProxy带有方向信息,必须先将图片旋转到与预览一致的方向,否则裁剪会出现错位。

2. 获取PreviewView的实际显示尺寸

PreviewView是约束布局,需要确保布局完成后再获取真实宽高,避免拿到0值。

3. 计算裁剪区域并缩放至预览尺寸

相机拍摄的原图比例和预览区域比例一致,但尺寸更大,需要先裁剪出和预览比例匹配的区域,再缩放至预览的精确尺寸。

修改后的完整拍照代码如下:

private fun takePhoto() {
    imageCapture.takePicture(ContextCompat.getMainExecutor(this), object :
        ImageCapture.OnImageCapturedCallback() {
        override fun onCaptureSuccess(image: ImageProxy) {
            // 处理图片旋转,得到和预览同方向的Bitmap
            val rotationDegrees = image.imageInfo.rotationDegrees
            val buffer = image.planes[0].buffer
            val bytes = ByteArray(buffer.remaining())
            buffer.get(bytes)
            var sourceBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
            
            val matrix = Matrix()
            matrix.postRotate(rotationDegrees.toFloat())
            sourceBitmap = Bitmap.createBitmap(sourceBitmap, 0, 0, sourceBitmap.width, sourceBitmap.height, matrix, true)

            // 获取PreviewView的实际尺寸
            val previewWidth = binding.preview.width
            val previewHeight = binding.preview.height

            if (previewWidth == 0 || previewHeight == 0) {
                // 布局未完成,延迟处理
                binding.preview.post {
                    processBitmapToPreviewSize(sourceBitmap, binding.preview.width, binding.preview.height)
                }
            } else {
                processBitmapToPreviewSize(sourceBitmap, previewWidth, previewHeight)
            }

            image.close()
        }

        override fun onError(exception: ImageCaptureException) {
            Log.e(TAG, "Photo capture failed: ${exception.message}", exception)
        }
    })
}

private fun processBitmapToPreviewSize(sourceBitmap: Bitmap, previewWidth: Int, previewHeight: Int) {
    val sourceWidth = sourceBitmap.width
    val sourceHeight = sourceBitmap.height

    // 计算预览和原图的比例
    val previewRatio = previewWidth.toFloat() / previewHeight.toFloat()
    val sourceRatio = sourceWidth.toFloat() / sourceHeight.toFloat()

    var targetBitmap: Bitmap

    if (sourceRatio > previewRatio) {
        // 原图更宽,裁剪左右两侧
        val cropWidth = (sourceHeight * previewRatio).toInt()
        val startX = (sourceWidth - cropWidth) / 2
        targetBitmap = Bitmap.createBitmap(sourceBitmap, startX, 0, cropWidth, sourceHeight)
    } else {
        // 原图更高,裁剪上下两侧
        val cropHeight = (sourceWidth / previewRatio).toInt()
        val startY = (sourceHeight - cropHeight) / 2
        targetBitmap = Bitmap.createBitmap(sourceBitmap, 0, startY, sourceWidth, cropHeight)
    }

    // 缩放至PreviewView的精确尺寸
    targetBitmap = Bitmap.createScaledBitmap(targetBitmap, previewWidth, previewHeight, true)

    // 显示结果
    binding.photoPreview.isVisible = true
    binding.photoPreview.setImageBitmap(targetBitmap)

    // 回收临时Bitmap避免内存泄漏
    sourceBitmap.recycle()
}

额外优化(可选)

初始化ImageCapture时可以尝试指定目标分辨率,减少后续裁剪的计算量,但由于设备支持的分辨率有限,裁剪仍是更可靠的方案:

val imageCapture = ImageCapture.Builder()
    .setTargetResolution(Size(previewWidth, previewHeight))
    .build()

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 10:37:32