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

Android Camera2 SurfaceView预览画面变形问题求助

解决Android Camera2预览画面变形问题

你的问题核心很明确:相机输出的预览尺寸宽高比和SurfaceView的显示宽高比不匹配,导致竖屏拉伸、旋转后挤压的情况。下面是一步步的解决方案,直接对应你的代码修改:

1. 编写工具方法:选择匹配比例的最优相机尺寸

首先,我们需要从相机支持的输出尺寸列表里,挑选出和SurfaceView显示比例一致、且分辨率最高的尺寸(保证预览清晰度)。在你的CameraActivity2里添加这个方法:

private fun chooseOptimalSize(
    choices: Array<Size>,
    viewWidth: Int,
    viewHeight: Int,
    maxWidth: Int,
    maxHeight: Int,
    targetAspectRatio: Size
): Size {
    // 筛选出符合目标宽高比的尺寸(允许微小误差避免整数计算偏差)
    val suitableSizes = mutableListOf<Size>()
    val targetRatio = targetAspectRatio.width.toFloat() / targetAspectRatio.height.toFloat()

    for (size in choices) {
        val sizeRatio = size.width.toFloat() / size.height.toFloat()
        if (Math.abs(sizeRatio - targetRatio) < 0.01 &&
            size.width <= maxWidth && size.height <= maxHeight) {
            suitableSizes.add(size)
        }
    }

    // 优先选分辨率最高的合适尺寸;没有匹配项则 fallback 到第一个尺寸
    return suitableSizes.maxByOrNull { it.width * it.height } ?: choices[0]
}

2. 修改相机会话启动逻辑:动态匹配尺寸

在startCameraSession方法里,不要直接取yuvSizes.first(),而是先获取SurfaceView的实际显示尺寸,计算比例后选择最优尺寸:

@SuppressLint("MissingPermission")
private fun startCameraSession() {
    with (cameraManager) {
        if (cameraIdList.isEmpty()) {
            Toast.makeText(this@CameraActivity2, "无可用相机", Toast.LENGTH_SHORT).show()
            return
        }
        val firstCamera = cameraIdList[0]
        openCamera(firstCamera, object: CameraDevice.StateCallback() {
            override fun onDisconnected(p0: CameraDevice) {
                Log.d(TAG, "Camera is Disconnected")
            }

            override fun onError(p0: CameraDevice, p1: Int) {
                Log.d(TAG, "Camera Error: $p1")
            }

            override fun onOpened(cam: CameraDevice) {
                camera = cam
                cameraCharacteristics = getCameraCharacteristics(camera.id)
                val configMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
                cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)?.let { maximumZoomLevel = it * 10 }
                if (configMap == null) {
                    Log.d(TAG,"No config map for camera: ${camera.id}")
                    return
                }
                val yuvSizes = configMap.getOutputSizes(ImageFormat.YUV_420_888)
                if (yuvSizes.isEmpty()) {
                    Log.d(TAG,"No sizes found for format YUV")
                    return
                }

                // --- 新增:计算并选择最优预览尺寸 ---
                val viewWidth = surfaceView.width
                val viewHeight = surfaceView.height
                val displayRotation = windowManager.defaultDisplay.rotation
                val swappedDimensions = areDimensionsSwapped(displayRotation)
                
                // 根据屏幕旋转,确定目标宽高比的正确维度
                val targetAspectRatio = if (swappedDimensions) {
                    Size(viewHeight, viewWidth)
                } else {
                    Size(viewWidth, viewHeight)
                }

                // 获取传感器的最大分辨率作为尺寸上限
                val sensorWidth = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_WIDTH) ?: 1920
                val sensorHeight = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_HEIGHT) ?: 1080

                // 选择最优尺寸
                previewSize = chooseOptimalSize(
                    yuvSizes,
                    viewWidth,
                    viewHeight,
                    sensorWidth,
                    sensorHeight,
                    targetAspectRatio
                )
                decodeSize = previewSize
                // --- 新增结束 ---

                val rotatedPreviewWidth = if (swappedDimensions) previewSize.height else previewSize.width
                val rotatedPreviewHeight = if (swappedDimensions) previewSize.width else previewSize.height

                // --- 修改:不再用setSizeFromLayout,后续通过LayoutParams调整 ---
                // surfaceView.holder.setSizeFromLayout()

                // 配置Image Reader
                imageReader = ImageReader.newInstance(rotatedPreviewWidth, rotatedPreviewHeight, YUV_420_888, 10)
                imageReader.setOnImageAvailableListener(
                    { imageReader ->
                        val image = imageReader.acquireLatestImage()
                        Log.d(TAG, "Image Available: H (${image?.height}), W (${image?.width})")
                        Log.d(TAG, "Surface Size: H (${surfaceView.height}), W (${surfaceView.width})")
                        image?.close()
                    }, Handler {true})
                previewSurface = surfaceView.holder.surface
                recordingSurface = imageReader.surface
                captureCallback = object : CameraCaptureSession.StateCallback() {
                    override fun onConfigureFailed(session: CameraCaptureSession) {
                    }

                    override fun onConfigured(session: CameraCaptureSession) {
                        previewRequestBuilder = camera.createCaptureRequest(TEMPLATE_PREVIEW).apply {
                            addTarget(recordingSurface)
                            addTarget(previewSurface)
                        }
                        session.setRepeatingRequest(previewRequestBuilder.build(), object: CameraCaptureSession.CaptureCallback() {}, Handler { true })
                        captureSession = session
                    }
                }
                camera.createCaptureSession(mutableListOf(previewSurface, recordingSurface), captureCallback, Handler { true })
            }
        }, Handler { true })
    }
}

3. 调整SurfaceView布局:保持画面比例

在surfaceReadyCallback的surfaceChanged方法里,根据选中的预览尺寸调整SurfaceView的布局,避免拉伸:

private val surfaceReadyCallback = object: SurfaceHolder.Callback {
    override fun surfaceChanged(p0: SurfaceHolder?, format: Int, width: Int, height: Int) {
        Log.d(TAG, "Surface Changed ${p0?.surface.toString()}")
        // --- 新增:调整SurfaceView尺寸匹配预览比例 ---
        if (::previewSize.isInitialized) {
            val previewRatio = previewSize.width.toFloat() / previewSize.height.toFloat()
            val viewRatio = width.toFloat() / height.toFloat()
            val layoutParams = surfaceView.layoutParams

            if (previewRatio > viewRatio) {
                // 相机比例更宽:View高度填满父布局,宽度按比例计算(左右留黑边)
                layoutParams.height = height
                layoutParams.width = (height * previewRatio).toInt()
            } else {
                // 相机比例更高:View宽度填满父布局,高度按比例计算(上下留黑边)
                layoutParams.width = width
                layoutParams.height = (width / previewRatio).toInt()
            }
            // 居中显示
            surfaceView.layoutParams = layoutParams
            // 重启相机会话适配新的Surface尺寸
            startCameraSession()
        }
        // --- 新增结束 ---
    }

    override fun surfaceDestroyed(p0: SurfaceHolder?) {
        Log.d(TAG, "Surface Destroyed ${p0?.surface.toString()}")
        // 销毁相机资源,避免内存泄漏
        camera.close()
    }

    override fun surfaceCreated(p0: SurfaceHolder?) {
        startCameraSession()
    }
}

关键逻辑说明

  • 比例匹配:通过chooseOptimalSize确保相机输出的尺寸比例和SurfaceView显示比例一致,从根源避免拉伸。
  • 布局调整:当Surface尺寸变化(比如屏幕旋转)时,重新计算SurfaceView的布局参数,让画面按比例显示,多余区域留黑边而不是拉伸。
  • 旋转适配:areDimensionsSwapped方法已经处理了传感器方向和屏幕旋转的维度交换,我们在计算目标比例时复用了这个逻辑,保证旋转后比例依然正确。

这样修改后,你的预览画面就会保持正确的比例,不会再出现竖屏拉伸、旋转挤压的问题了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:26:28