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
相关产品推荐
相关产品推荐

