Skip to content

相机扭曲的解决办法

发生相机扭曲的主要原因是surfaceview的宽高比与相机输出图像的最佳宽高比不一致,相机输出图像适配surfaceview的时候被拉伸导致的扭曲。

步骤1:获取相机支持的输出比例

kotlin
// 在Fragment中初始化预览尺寸
private fun initPreviewSize() {
    val characteristics = cameraManager.getCameraCharacteristics(cameraId)
    previewSize = getPreviewOutputSize(
        binding.surfaceView.display, 
        characteristics, 
        SurfaceHolder::class.java
    )
}

// 核心尺寸选择算法
private fun getPreviewOutputSize(
    display: Display?,
    characteristics: CameraCharacteristics,
    targetClass: Class<*>
): Size {
    // 1. 获取相机支持的所有分辨率
    val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    val outputSizes = config.getOutputSizes(targetClass)

    // 2. 计算显示区域宽高比
    val displaySize = Point().apply { display?.getSize(this) }
    val displayRatio = displaySize.x.toFloat() / displaySize.y

    // 3. 选择最接近屏幕比例的相机分辨率
    return outputSizes.minByOrNull { 
        abs((it.width.toFloat()/it.height) - displayRatio)
    } ?: outputSizes.maxByOrNull { it.width * it.height }!!
}

原理说明:通过比较相机支持的分辨率宽高比与屏幕显示比例的差异,选择最匹配的预览尺寸


步骤2:设置视图动态宽高比

kotlin
// 在SurfaceView创建时绑定比例
private fun setAspectRatio() {
    binding.surfaceView.setAspectRatio(previewSize.width, previewSize.height)
}

// AutoFitSurfaceView核心实现
class AutoFitSurfaceView(...) : SurfaceView(...) {
    private var aspectRatio = 0f

    fun setAspectRatio(width: Int, height: Int) {
        aspectRatio = width.toFloat() / height
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        
        // 动态计算适配尺寸
        when {
            aspectRatio == 0f -> super.onMeasure(...)
            width > height -> handleLandscape(width, height)
            else -> handlePortrait(width, height)
        }
    }

    private fun handleLandscape(width: Int, height: Int) {
        val expectedHeight = (width / aspectRatio).roundToInt()
        setMeasuredDimension(width, expectedHeight.coerceAtMost(height))
    }

    private fun handlePortrait(width: Int, height: Int) {
        val expectedWidth = (height * aspectRatio).roundToInt()
        setMeasuredDimension(expectedWidth.coerceAtMost(width), height)
    }
}

核心机制:基于相机分辨率比例动态计算视图尺寸,保证画面始终居中完整显示


步骤3:处理设备方向变化

kotlin
// 在Fragment中监听方向变化
private val orientationListener = object : OrientationEventListener(context) {
    override fun onOrientationChanged(orientation: Int) {
        val newRotation = when {
            orientation in 45..135 -> Surface.ROTATION_270
            orientation in 135..225 -> Surface.ROTATION_180
            orientation in 225..315 -> Surface.ROTATION_90
            else -> Surface.ROTATION_0
        }
        
        if (newRotation != lastRotation) {
            adjustPreviewForRotation(newRotation)
        }
    }
}

private fun adjustPreviewForRotation(rotation: Int) {
    // 1. 重新计算传感器方向
    val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
    val totalRotation = (sensorOrientation + rotation) % 360

    // 2. 更新预览方向
    captureSession?.setDisplayOrientation(totalRotation)

    // 3. 重新设置视图比例
    setAspectRatio()
}

方向适配逻辑

  1. 计算传感器物理方向与设备旋转的合成方向
  2. 通过setDisplayOrientation同步预览方向
  3. 强制重绘视图保持比例正确

步骤4:建立预览会话时验证参数

kotlin
private fun createCaptureSession() {
    val targets = listOf(surfaceView.holder.surface)
    
    cameraDevice?.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
        override fun onConfigured(session: CameraCaptureSession) {
            // 验证实际预览尺寸
            val sessionParams = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
            val activeSize = sessionParams.surface?.let { 
                it.getSize() // 实际生效的分辨率
            }
            
            if (activeSize != previewSize) {
                Log.w(TAG, "实际预览尺寸($activeSize)与预期($previewSize)不符")
                adjustPreviewSize(activeSize)
            }
        }
    }, null)
}

尺寸验证机制:在会话建立后检查实际生效的预览尺寸,必要时动态调整


步骤5:异常情况处理方案

kotlin
// 尺寸不匹配时的自动修复
private fun adjustPreviewSize(actualSize: Size) {
    // 1. 关闭当前会话
    closeCamera()

    // 2. 更新目标尺寸
    previewSize = actualSize

    // 3. 重新打开相机
    openCamera(cameraId).also {
        binding.surfaceView.setAspectRatio(actualSize.width, actualSize.height)
    }
}

// 在AutoFitSurfaceView中添加保护逻辑
override fun setAspectRatio(width: Int, height: Int) {
    if (width == 0 || height == 0) {
        throw IllegalArgumentException("无效的尺寸参数: ${width}x$height")
    }
    aspectRatio = width.toFloat() / height
}

容错机制

  • 自动检测尺寸偏差
  • 异常参数校验
  • 安全重连机制

实施效果验证

测试用例1:基础比例适配

设备类型相机分辨率屏幕比例预期效果
16:9手机竖屏1920x10809:16上下黑边,无拉伸
4:3平板横屏1280x7204:3左右黑边,比例正确

测试用例2:动态方向适配

操作步骤预期效果
竖屏启动后旋转至横屏画面自动旋转并保持比例
快速连续旋转设备画面平滑过渡无撕裂

测试用例3:异常场景恢复

异常类型系统响应
相机返回非常规尺寸自动调整视图比例
参数传0触发异常抛出明确错误信息

通过以上五步实施方案,可系统性地解决Android相机预览画面扭曲问题。建议配合GPU呈现模式分析工具实时监控渲染性能,确保方案在不同设备上的兼容性。

Released under the MIT License.