Android Camera开发详解

Android Camera开发详解

一、Camera 发展历史与三代架构

1.1 三代架构总览

Camera1(已废弃) Camera2(现行标准) CameraX(Google 推荐)
推出时间 Android 2.3(API 1) Android 5.0(API 21) Android 10+(API 24+)
封装层级 高度封装,简单但受限 低级 API,全功能但复杂 高级封装,简单且统一
回调方式 Camera.PreviewCallback ImageReader + CaptureSession Observer 模式
生命周期 需手动管理 需手动管理 自动绑定 LifecycleOwner
配置复用 困难 手动配置每个请求 自动复用配置
厂商适配 厂商负责 厂商负责(碎片化严重) Google 官方兜底适配
视频拍摄 MediaRecorder MediaRecorder / ImageReader VideoCapture

二、Camera1 API

2.1 核心流程

undefined 复制代码
SurfaceView / TextureView
    ↓
Camera.open() → Camera
    ↓
Camera.setPreviewDisplay() / setPreviewTexture()
    ↓
Camera.startPreview()
    ↓
Camera.takePicture() / Camera.PreviewCallback
    ↓
Camera.stopPreview()
    ↓
Camera.release()

2.2 完整代码示例

java 复制代码
public class Camera1Activity extends AppCompatActivity
        implements TextureView.SurfaceTextureListener {

    private Camera mCamera;
    private TextureView mTextureView;
    private boolean mPreviewRunning = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mTextureView = new TextureView(this);
        mTextureView.setSurfaceTextureListener(this);
        setContentView(mTextureView);
    }

    // TextureView 准备好后打开摄像头
    @Override
    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int w, int h) {
        mCamera = Camera.open();  // 默认打开后置摄像头
        try {
            Camera.Parameters params = mCamera.getParameters();
            // 设置预览尺寸
            List<Camera.Size> supportedSizes = params.getSupportedPreviewSizes();
            Camera.Size optimalSize = getOptimalPreviewSize(supportedSizes, w, h);
            params.setPreviewSize(optimalSize.width, optimalSize.height);

            // 设置拍照尺寸
            List<Camera.Size> supportedPicSizes = params.getSupportedPictureSizes();
            Camera.Size picSize = supportedPicSizes.get(0);  // 通常最大尺寸在最后
            params.setPictureSize(picSize.width, picSize.height);

            // 设置对焦模式(自动对焦)
            List<String> focusModes = params.getSupportedFocusModes();
            if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
                params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
            }

            mCamera.setParameters(params);
            mCamera.setPreviewTexture(surface);
            mCamera.startPreview();
            mPreviewRunning = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int w, int h) {}

    @Override
    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}

    // ════════════════ 拍照 ════════════════
    public void takePicture() {
        if (mCamera != null) {
            mCamera.takePicture(null, null, (data, camera) -> {
                // data 即 JPEG 字节数组,保存到文件
                FileOutputStream fos = null;
                try {
                    fos = new FileOutputStream("/sdcard/photo.jpg");
                    fos.write(data);
                    fos.close();
                    Log.d("Camera1", "Photo saved: " + data.length + " bytes");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 拍完继续预览
                camera.startPreview();
            });
        }
    }

    // ════════════════ 预览回调(实时帧处理)═══════════════
    public void startPreviewCallback() {
        mCamera.setPreviewCallback((data, camera) -> {
            // data 是 NV21/YV12 格式的原始预览帧
            // 可以在这里做实时分析(如人脸检测、色值统计)
            processFrame(data);
        });
    }

    // ════════════════ 计算最佳预览尺寸 ════════════════
    private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double TARGET_RATIO = (double) w / h;
        Camera.Size optimal = null;
        double minDiff = Double.MAX_VALUE;
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - TARGET_RATIO) < minDiff) {
                optimal = size;
                minDiff = Math.abs(ratio - TARGET_RATIO);
            }
        }
        return optimal;
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }
}

2.3 Camera1 参数配置

java 复制代码
Camera.Parameters params = mCamera.getParameters();

// ─── 预览尺寸 ───
params.setPreviewSize(width, height);
params.setPreviewFormat(ImageFormat.NV21);  // Camera1 默认格式

// ─── 拍照尺寸 ───
params.setPictureSize(width, height);
params.setPictureFormat(ImageFormat.JPEG);

// ─── 对焦模式 ───
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);      // 自动对焦
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);  // 持续对焦
params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);   // 无限远(适合远景)
params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);      // 固定焦距(眼镜/工控场景)

// ─── 闪光灯 ───
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
params.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
params.setFlashMode(Camera.Parameters.FLASH_MODE_ON);

// ─── 场景模式 ───
params.setSceneMode(Camera.Parameters.SCENE_MODE_NIGHT);    // 夜景模式
params.setSceneMode(Camera.Parameters.SCENE_MODE_ACTION);     // 运动模式(缩短曝光)

// ─── 曝光补偿 ───
params.setExposureCompensation(value);  // value范围由 getMinExposureCompensation() 和 getMaxExposureCompensation() 确定

三、Camera2 API

3.1 架构核心概念

Camera2 是对硬件抽象层(HAL1)的重新设计,引入了**管道(Pipeline)**模型:

undefined 复制代码
应用层
    ↓ CaptureRequest
CameraCaptureSession(会话)
    ↓
CameraDevice(设备)
    ↓
CameraCharacteristics(摄像头特性)
    ↓
Camera HAL(硬件抽象层)
    ↓
CMOS Sensor(实际硬件)

四个关键对象:

  • CameraManager:摄像头管理器(系统服务),用于枚举和打开摄像头
  • CameraDevice:摄像头设备,代表一个物理摄像头
  • CameraCaptureSession:一次拍摄会话,管理输入/输出 Surface
  • CaptureRequest:一次具体的拍摄请求(预览/拍照/录像)

3.2 完整预览 + 拍照代码

kotlin 复制代码
class Camera2Activity : AppCompatActivity() {

    private lateinit var textureView: TextureView
    private lateinit var cameraManager: CameraManager
    private var cameraDevice: CameraDevice? = null
    private var captureSession: CameraCaptureSession? = null
    private var imageReader: ImageReader? = null
    private var backgroundHandler: Handler? = null
    private var cameraId: String = "0"  // 0=后置, 1=前置

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        textureView = TextureView(this)
        setContentView(textureView)

        cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
        startBackgroundThread()
    }

    // ════════════════ 打开摄像头 ════════════════
    private fun openCamera() {
        try {
            // 获取后置摄像头ID
            for (id in cameraManager.cameraIdList) {
                val characteristics = cameraManager.getCameraCharacteristics(id)
                val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
                if (facing == CameraCharacteristics.LENS_FACING_BACK) {
                    cameraId = id
                    break
                }
            }

            // 检查相机权限
            if (checkSelfPermission(Manifest.permission.CAMERA)
                    != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(arrayOf(Manifest.permission.CAMERA), 101)
                return
            }

            // 打开摄像头
            cameraManager.openCamera(cameraId, stateCallback, backgroundHandler)

        } catch (e: CameraAccessException) {
            e.printStackTrace()
        }
    }

    private val stateCallback = object : CameraDevice.StateCallback() {
        override fun onOpened(camera: CameraDevice) {
            cameraDevice = camera
            createPreviewSession()  // 摄像头打开后创建预览会话
        }

        override fun onDisconnected(camera: CameraDevice) {
            camera.close()
            cameraDevice = null
        }

        override fun onError(camera: CameraDevice, error: Int) {
            camera.close()
            cameraDevice = null
        }
    }

    // ════════════════ 创建预览会话 ════════════════
    private fun createPreviewSession() {
        val texture = textureView.surfaceTexture ?: return
        texture.setDefaultBufferSize(1280, 720)  // 预览分辨率

        // ImageReader 用于接收拍照数据
        imageReader = ImageReader.newInstance(
            1280, 720,
            ImageFormat.JPEG, 2
        ).apply {
            setOnImageAvailableListener({ reader ->
                val image = reader.acquireLatestImage()
                image?.let {
                    val buffer = it.planes[0].buffer
                    val bytes = ByteArray(buffer.remaining())
                    buffer.get(bytes)
                    savePhoto(bytes)  // 保存 JPEG
                    it.close()
                }
            }, backgroundHandler)
        }

        val surfaces = listOf(
            Surface(texture),
            imageReader!!.surface
        )

        // 创建 CaptureSession(这里传入所有需要用到的 Surface)
        cameraDevice?.createCaptureSession(
            surfaces,
            object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    captureSession = session
                    startPreview()  // 开始预览
                }

                override fun onConfigureFailed(session: CameraCaptureSession) {
                    Log.e("Camera2", "Session配置失败")
                }
            },
            backgroundHandler
        )
    }

    // ════════════════ 开始预览 ════════════════
    private fun startPreview() {
        val texture = textureView.surfaceTexture ?: return

        // 构建预览请求(Template)
        val previewRequest = cameraDevice?.createCaptureRequest(
            CameraDevice.TEMPLATE_PREVIEW  // 预览模板
        )?.apply {
            // 添加预览 Surface
            addTarget(Surface(texture))
            // 自动对焦
            set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
            // 自动曝光
            set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
        }?.build()

        previewRequest?.let {
            captureSession?.setRepeatingRequest(it, null, backgroundHandler)
        }
    }

    // ════════════════ 拍照 ════════════════
    fun takePicture() {
        val captureBuilder = cameraDevice?.createCaptureRequest(
            CameraDevice.TEMPLATE_STILL_CAPTURE  // 拍照模板
        )?.apply {
            addTarget(imageReader!!.surface)  // 输出到 ImageReader
            set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
            set(CaptureRequest.JPEG_ORIENTATION, 90)  // 旋转角度
        }?.build()

        captureBuilder?.let {
            captureSession?.capture(it, object : CameraCaptureSession.CaptureCallback() {
                override fun onCaptureCompleted(
                    session: CameraCaptureSession,
                    request: CaptureRequest, result: TotalCaptureResult
                ) {
                    Log.d("Camera2", "拍照完成")
                    // 拍完后回到预览
                    startPreview()
                }
            }, backgroundHandler)
        }
    }

    private fun savePhoto(data: ByteArray) {
        val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
            "photo_${System.currentTimeMillis()}.jpg")
        FileOutputStream(file).use { it.write(data) }
    }

    // ════════════════ 后台线程 ════════════════
    private fun startBackgroundThread() {
        val thread = HandlerThread("CameraBackground").also { it.start() }
        backgroundHandler = Handler(thread.looper)
    }
}

3.3 Camera2 核心 API 对照表

任务 Camera2 API
枚举摄像头 CameraManager.getCameraIdList() + getCameraCharacteristics()
打开摄像头 CameraManager.openCamera()
查询能力 CameraCharacteristics.get()
创建 Session CameraDevice.createCaptureSession()
预览请求 CameraDevice.createCaptureRequest(TEMPLATE_PREVIEW)
拍照请求 CameraDevice.createCaptureRequest(TEMPLATE_STILL_CAPTURE)
设置对焦 CaptureRequest.CONTROL_AF_MODE
设置曝光 CaptureRequest.CONTROL_AE_MODE / CONTROL_AE_EXPOSURE_COMPENSATION
设置闪光灯 CaptureRequest.FLASH_MODE
设置白平衡 CaptureRequest.CONTROL_AWB_MODE
设置ISO CaptureRequest.SENSOR_SENSITIVITY(需手动曝光模式)
设置快门时间 CaptureRequest.SENSOR_EXPOSURE_TIME(纳秒,需手动曝光)
设置缩放 CaptureRequest.SCALER_CROP_REGION
获取帧数据 ImageReader + Surface
录像 MediaRecorder(搭配 Surface input)

3.4 Camera2 关键参数查询

kotlin 复制代码
// ─── 获取摄像头特性 ───
val characteristics = cameraManager.getCameraCharacteristics(cameraId)

// ─── 支持的 Stream 配置 ───
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

// ─── 支持的预览/拍照尺寸 ───
val previewSizes = map.getOutputSizes(SurfaceTexture::class.java)
val pictureSizes = map.getOutputSizes(ImageFormat.JPEG)

// ─── 支持的对焦模式 ───
val afModes = characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)
// CONTROL_AF_MODE_OFF, CONTROL_AF_MODE_AUTO, CONTROL_AF_MODE_CONTINUOUS_PICTURE ...

// ─── 支持的闪光灯 ───
val flashAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)

// ─── 对焦测光区域 ───
val afRegion = MeteringRectangle(
    x, y, width, height,
    MeteringRectangle.METERING_WEIGHT_MAX  // 权重
)
// 在 CaptureRequest 中设置:set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(afRegion))
// 同时需要:set(CONTROL_AE_REGIONS, ...) + set(CONTROL_AF_MODE, CONTROL_AF_MODE_AUTO)

// ─── 缩放范围 ───
val zoomRange = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_ZOOM_RATIO_RANGE)
// zoomRatioRange.min / max(1.0x ~ 10.0x+)

四、CameraX API

4.1 为什么选择 CameraX

CameraX 是 Google 在 2019 年推出的新 API,解决了 Camera2 的三个核心痛点:

  1. 碎片化适配:Camera2 各厂商实现不一致,CameraX 统一兜底
  2. 生命周期管理:Camera2 需手动管理,CameraX 自动绑定 LifecycleOwner
  3. 配置复用:CameraX 自动管理 Session 配置的复用和重建

4.2 CameraX 完整代码(预览 + 拍照 + 分析)

kotlin 复制代码
class CameraXActivity : AppCompatActivity() {

    private lateinit var cameraController: ProcessCameraProvider
    private lateinit var imageAnalysis: ImageAnalysis
    private val imageCapture = ImageCapture.Builder()
        .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)  // 或 CAPTURE_MODE_MINIMIZE_LATENCY
        .build()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewFinder = findViewById<PreviewView>(R.id.viewFinder)

        // ─── 绑定用例 ───
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            // ─── Preview ───
            val preview = Preview.Builder()
                .setTargetResolution(Size(1280, 720))
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }

            // ─── ImageAnalysis(实时帧分析)══════════════
            imageAnalysis = ImageAnalysis.Builder()
                .setTargetResolution(Size(640, 480))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor) { imageProxy ->
                        // imageProxy 是 YUV_420_888 格式
                        processImage(imageProxy)
                        imageProxy.close()  // 必须关闭,否则 CameraX 阻塞
                    }
                }

            // ─── 绑定到 Lifecycle ───
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this,           // ← 自动管理生命周期
                    CameraSelector.DEFAULT_BACK_CAMERA,
                    preview,
                    imageCapture,
                    imageAnalysis
                )
            } catch (e: Exception) {
                Log.e("CameraX", "Use case binding failed", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    // ════════════════ 拍照 ════════════════
    private val cameraExecutor = Executors.newSingleThreadExecutor()

    fun takePhoto() {
        val outputOptions = ImageCapture.OutputFileOptions.Builder(
            File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), "photo.jpg"
            )
        ).build()

        imageCapture.takePicture(
            outputOptions,
            cameraExecutor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    Log.d("CameraX", "Photo saved: ${output.savedUri}")
                }
                override fun onError(exception: ImageCaptureException) {
                    Log.e("CameraX", "Photo capture failed", exception)
                }
            }
        )
    }

    // ════════════════ 实时帧分析 ════════════════
    @androidx.annotation.OptIn(ExperimentalGetImage::class)
    private fun processImage(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            // 方法1:直接用 Image
            val buffer = imageProxy.planes[0].buffer
            val data = ByteArray(buffer.remaining())
            buffer.get(data)

            // 方法2:转 Bitmap(注意性能)
            val bitmap = imageProxy.toBitmap()

            // 方法3:用 ML Kit 做图像分析(示例)
            // val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // run inference here
        }
    }

    // ════════════════ 切换前后摄像头 ════════════════
    fun switchCamera() {
        val cameraSelector = if (currentLens == CameraSelector.LENS_FACING_BACK) {
            CameraSelector.DEFAULT_FRONT_CAMERA
        } else {
            CameraSelector.DEFAULT_BACK_CAMERA
        }
        // 重新绑定即可
    }
}

4.3 CameraX 核心配置

kotlin 复制代码
// ─── ImageCapture 配置 ───
ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)   // 高质量(适合拍照)
    // .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 低延迟(适合连拍)
    .setTargetResolution(Size(1280, 720))
    .setTargetRotation(windowManager.defaultDisplay.rotation)
    .setFlashMode(ImageCapture.FLASH_MODE_AUTO / FLASH_MODE_ON / FLASH_MODE_OFF)
    .build()

// ─── Preview 配置 ───
Preview.Builder()
    .setTargetResolution(Size(1280, 720))
    .setTargetRotation(windowManager.defaultDisplay.rotation)
    .build()

// ─── ImageAnalysis 配置 ───
ImageAnalysis.Builder()
    .setTargetResolution(Size(640, 480))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    // STRATEGY_KEEP_ONLY_LATEST: 只保留最新帧,丢弃旧帧(适合实时分析)
    // STRATEGY_BLOCK_PRODUCER: 阻塞生产者直到消费者处理完(保证不丢帧,但可能卡顿)
    .build()

// ─── 选择摄像头 ───
CameraSelector.DEFAULT_BACK_CAMERA      // 后置
CameraSelector.DEFAULT_FRONT_CAMERA     // 前置
CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK)
    .build()

4.4 CameraX 录像(VideoCapture)

kotlin 复制代码
// ─── 录像绑定 ───
private var videoCapture: VideoCapture<Recorder>? = null

val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.HD))
    .build()

videoCapture = VideoCapture.withOutput(recorder)

cameraProvider.bindToLifecycle(
    this,
    CameraSelector.DEFAULT_BACK_CAMERA,
    preview,
    videoCapture
)

// ─── 开始录像 ───
fun startRecording() {
    val file = File(outputDir, "video_${System.currentTimeMillis()}.mp4")
    val outputOptions = FileOutputOptions.Builder(file).build()

    videoCapture?.startRecording(
        outputOptions,
        cameraExecutor,
        object : VideoCapture.OnVideoSavedCallback {
            override fun onVideoSaved(output: VideoCapture.OutputFileResults) {
                Log.d("CameraX", "Video saved: ${file.absolutePath}")
            }
            override fun onError(code: Int, message: String) {
                Log.e("CameraX", "Video save error: $message")
            }
        }
    )
}

// ─── 停止录像 ───
fun stopRecording() {
    videoCapture?.stopRecording()
}

五、三代 Camera API 横向对比

5.1 逐维度对比

维度 Camera1 Camera2 CameraX
代码行数(预览+拍照) ~80行 ~200行 ~50行
生命周期管理 手动 手动 自动绑定
横竖屏切换 需重建 需重建 自动处理
多摄像头切换 Camera.open(id) CameraManager.openCamera() bindToLifecycle 重建
手动曝光(ISO/快门) 部分支持 完全支持 有限支持
Raw 格式输出 不支持 支持 不支持
ZSL(零快门延迟) 原生 手动配置 自动
厂商适配成本 高(碎片化)
最低 Android 版本 API 1 API 21(大部分设备 API 26+) API 24(部分 API 21)

5.2 选型建议

undefined 复制代码
┌─────────────────────────────────────────────────────┐
│           如何选择 Camera API?                       │
├─────────────────────────────────────────────────────┤
│  只需简单拍照/预览,新项目 / 快速上线                  │
│         → CameraX(Google 推荐,代码量最少)         │
├─────────────────────────────────────────────────────┤
│  需要完全控制:ISO/快门/对焦/Raw输出/多摄像头          │
│         → Camera2(最底层,功能最全)                │
├─────────────────────────────────────────────────────┤
│  旧项目 / 维护已有 Camera1 代码                      │
│  临时支持 / 简单场景                                │
│         → Camera1(仅作为过渡)                      │
├─────────────────────────────────────────────────────┤
│  智能眼镜 / 工控设备 / 嵌入式 Android                 │
│  需要 Sensor 寄存器级控制、定制 ISP 参数               │
│         → Camera2(必须,CameraX 无法满足)          │
└─────────────────────────────────────────────────────┘

六、常见问题与解决方案

Q1:预览变形(拉伸/压缩)

原因: 预览 TextureView 比例与摄像头输出比例不一致。

CameraX 解决:

kotlin 复制代码
// 方案1:设置 ScaleType(最简单)
viewFinder.scaleType = PreviewView.ScaleType.FIT_CENTER
// FIT_CENTER: 等比缩放,多余区域黑边(推荐预览)
// FILL_CENTER: 拉伸填满(预览OK,录像不建议)
// FIT_START/FIT_END: 等比,贴合一边

// 方案2:让 TextureView 尺寸与 Camera 输出比例一致
val cameraController = cameraProvider.bindToLifecycle(...)
val bindContext = cameraController.cameraInfo
val targetRatio = 16f / 9f

viewFinder.post {
    val params = viewFinder.layoutParams
    params.width = viewFinder.width
    params.height = (viewFinder.width / targetRatio).toInt()
    viewFinder.layoutParams = params
}

Camera1/Camera2 解决:

java 复制代码
// 在 onPreviewFrame 回调中手动缩放
canvas.drawBitmap(bitmap,
    null,  // 源区域(全帧)
    new RectF(0, 0, viewWidth, viewHeight),  // 目标区域
    new Paint(Paint.FILTER_BITMAP_FLAG)
);

Q2:预览方向错误(旋转90°/270°)

Camera1:

java 复制代码
Camera.Parameters params = mCamera.getParameters();
String orientation = params.get("orientation");
if (orientation != null) {
    params.set("orientation", "portrait");  // 前置摄像头
    mCamera.setParameters(params);
}
// Android 4.0+ 用 setDisplayOrientation
mCamera.setDisplayOrientation(90);  // 前置:270;后置:90

Camera2/CameraX:

kotlin 复制代码
val rotation = windowManager.defaultDisplay.rotation
// rotation: 0=竖屏, 90=横屏(右), 180=倒竖, 270=横屏(左)

// Camera2
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION,
    ORIENTATIONS.get(rotation))

// CameraX 自动处理,但录像时需要设置
val preview = Preview.Builder()
    .setTargetRotation(rotation)
    .build()

Q3:前置摄像头镜像

需求: 前置摄像头预览时文字是正的(镜像效果),拍照存的文件是镜像还是正的?

kotlin 复制代码
// CameraX:预览镜像
viewFinder.scaleX = -1f  // 水平镜像

// Camera2:预览时应用 Transform
val matrix = Matrix()
matrix.setScale(-1f, 1f, viewFinder.width / 2f, viewFinder.height / 2f)
textureView.setTransform(matrix)

// 关键点:镜像只影响预览,不影响 JPEG 拍照数据
// JPEG 内容永远是摄像头原始方向(前置默认镜像存储)
// 如需保存"正"的照片,需手动翻转字节数组

Q4:Camera2 无法打开(CameraAccessException)

错误类型排查:

undefined 复制代码
CAMERA_ERROR_EVICTED = 2         → 摄像头被其他应用占用
CAMERA_ERROR_SERVER_DIED = 100   → Camera 服务崩溃,重建 CameraManager
CAMERA_ERROR_MAX_CAMERAS_IN_USE = 4 → 超过最大摄像头数量
CAMERA_DISABLED = 1               → 摄像头被系统禁用(安全策略)
try {
    cameraManager.openCamera(cameraId, callback, handler)
} catch (e: CameraAccessException) {
    when (e.reason) {
        CameraAccessException.CAMERA_ERROR_EVICTED -> {
            // 摄像头被占用,提示用户关闭其他应用
        }
        CameraAccessException.CAMERA_ERROR_SERVER_DIED -> {
            // Camera 服务崩溃,等待系统恢复后重试
            Handler(Looper.getMainLooper()).postDelayed({
                cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
                openCamera()
            }, 1000)
        }
    }
}

Q5:CameraX bindToLifecycle 失败(IllegalArgumentException)

常见原因: LifecycleOwner 状态不对(如 Fragment 已 onDestroy 后尝试绑定)。

kotlin 复制代码
// ❌ 错误:在 onDestroyView 中绑定
override fun onDestroyView() {
    // 某些框架版本中,这里 still 有生命周期
    cameraProvider.unbindAll()  // 先解绑
    super.onDestroyView()
}

// ✅ 正确:绑定和解绑都基于 lifecycle
// bindToLifecycle(this as LifecycleOwner)
// this 是 AppCompatActivity(实现了 LifecycleOwner)

// 在 Fragment 中使用
lifecycleScope.launch {
    val cameraProvider = ProcessCameraProvider.getInstance(requireContext()).get()
    cameraProvider.bindToLifecycle(
        viewLifecycleOwner,  // ← 用 viewLifecycleOwner,不用 this
        cameraSelector,
        preview,
        imageCapture
    )
}

Q6:预览帧处理卡顿 / 丢帧

CameraX ImageAnalysis 丢帧原因与解决:

kotlin 复制代码
// 原因1:分析器处理太慢,CameraX 丢弃旧帧
// STRATEGY_KEEP_ONLY_LATEST:自动丢弃旧帧,不阻塞(推荐实时分析)

// 原因2:图片格式转换耗时
@androidx.annotation.OptIn(ExperimentalGetImage::class)
private fun processImage(imageProxy: ImageProxy) {
    // ❌ 不要在这里做复杂计算
    // ❌ 不要在这里转 Bitmap(太慢)
    // ✅ 在后台线程池处理
    val buffer = imageProxy.planes[0].buffer
    val data = ByteArray(buffer.remaining())
    buffer.get(data)

    cameraExecutor.submit {
        // 在独立线程处理,不阻塞 ImageAnalysis
        runInference(data)
    }
}

// 原因3:分辨率过高
imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(320, 240))  // ← 降低分析分辨率(预览用高清,分析用低分辨率)
    .build()

Q7:Camera1 在 Android 8.0+ 前置摄像头无法自动对焦

问题: Camera1 FOCUS_MODE_CONTINUOUS_PICTURE 在部分设备上对前置摄像头无效。

java 复制代码
// 解决1:降级到 FOCUS_MODE_AUTO(需要点击触发)
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);

// 解决2:手动设置焦点区域
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);

// 焦点区域(需要 Camera1 支持 FOCUS_MODE_AUTO)
// MeteringRectangle 格式:x, y, width, height, weight
Camera.Area focusArea = new Camera.Area(new Rect(-300, -300, 300, 300), 1000);
params.setFocusAreas(Arrays.asList(focusArea));
params.setMeteringAreas(Arrays.asList(focusArea));
mCamera.setParameters(params);

// 解决3:直接迁移到 Camera2(Camera1 的对焦 API 在部分厂商实现不完整)

Q8:CameraX 在 Android 10 以下无法保存照片

原因: Android 10(API 29)开始Scoped Storage,外部存储权限收紧。

kotlin 复制代码
// 方案1:使用 MediaStore(Android Q+ 推荐)
val contentValues = ContentValues().apply {
    put(MediaStore.MediaColumns.DISPLAY_NAME, "photo_${System.currentTimeMillis()}.jpg")
    put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
    }
}

val outputOptions = ImageCapture.OutputFileOptions.Builder(
    contentResolver,
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    contentValues
).build()

// 方案2:使用应用私有目录(无需权限)
val outputOptions = ImageCapture.OutputFileOptions.Builder(
    File(requireContext().filesDir, "photos").also { it.mkdirs() },
    "photo_${System.currentTimeMillis()}.jpg"
).build()

// 方案3:Legacy 兼容模式(在 AndroidManifest 中关闭 Scoped Storage)
// <application android:requestLegacyExternalStorage="true">
// 注意:这个属性在 Android 11+ 无效

Q9:Camera2 预览正常但拍照报 "No such file"

原因: Camera2 拍照用 ImageReader,如果 ImageReader 的 Surface 销毁后再拍照会失败。

kotlin 复制代码
// 错误写法:onResume 中 openCamera,在 onPause 中 release
// 如果 Activity 生命周期管理不好,ImageReader 可能已被回收

// 正确做法:使用 Lifecycle
// ProcessCameraProvider.bindToLifecycle 自动管理 ImageReader 生命周期
// Camera2 中也要确保 ImageReader 在 Session 重建时同步重建

// 检查 imageReader 是否已关闭
if (imageReader?.surface?.isValid != true) {
    Log.e("Camera2", "ImageReader surface 无效,重新创建")
    recreateImageReader()
}

Q10:CameraX 切换摄像头后预览卡死

原因: 切换摄像头时旧的 Session 资源未完全释放。

kotlin 复制代码
// 完整切换流程
fun switchCamera() {
    cameraProvider.unbindAll()  // ← 先解绑所有用例

    // 延迟一点再绑定(让底层资源完全释放)
    Handler(Looper.getMainLooper()).postDelayed({
        currentLens = if (currentLens == CameraSelector.DEFAULT_BACK_CAMERA) {
            CameraSelector.DEFAULT_FRONT_CAMERA
        } else {
            CameraSelector.DEFAULT_BACK_CAMERA
        }

        cameraProvider.bindToLifecycle(
            this,
            currentLens,
            preview,
            imageCapture,
            imageAnalysis
        )
    }, 300)
}

七、遇到的高频问题

Q1:Camera1 和 Camera2 的核心区别是什么?

参考答案:

Camera1 是 Android 早期 API,封装层次高,简单场景够用,但无法访问很多硬件能力(如手动曝光 Raw 输出)。Camera2 是完全重新设计的 API,采用管道模型(CameraDevice → CaptureSession → CaptureRequest),可以控制每一个细节(ISO、快门时间、对焦区域、闪光灯策略等),但代码复杂且厂商碎片化严重。Camera2 是 Camera1 的替代品,Camera1 已废弃(Android 10 后不再维护)。

Q2:CameraX 相比 Camera2 最大的优势是什么?有什么场景 CameraX 做不了?

参考答案:

CameraX 最大优势:①生命周期自动管理,不用担心 Activity 重建时摄像头泄漏;②Google 统一适配,解决厂商碎片化;③API 简洁,代码量减少 60%+。CameraX 做不到的事:①Raw 格式输出(需要 Camera2);②Sensor 级手动控制(手动 ISO/快门时间需要 Camera2);③完全自定义预览处理链(CameraX 是黑盒);④同时预览两个不同摄像头的画面(CameraX 底层仍是单 CameraDevice)。

Q3:Camera2 的 CaptureRequest 中的 CONTROL_AF_MODE 有哪些值?分别适用什么场景?

参考答案:

  • OFF:完全手动对焦,手动设置镜头位置(几乎不用)
  • AUTO:单次自动对焦,对焦一次后锁定(点击对焦场景)
  • CONTINUOUS_PICTURE:持续自动对焦,适用于拍照预览(推荐)
  • CONTINUOUS_VIDEO:持续对焦,优化为视频平滑对焦(录像模式)
  • EDOF(Extended Depth of Field):景深扩展(无需对焦,近远景都清晰,用于低端设备)

智能眼镜/工控设备通常用 CONTINUOUS_PICTUREFIXED(焦距固定,不自动对焦)。

Q4:CameraX 的 ImageAnalysis 在处理 YUV 数据时要注意什么?

参考答案:

必须调用 imageProxy.close() ,否则 CameraX 阻塞,无法继续下一帧;② YUV_420_888 格式,Y 平面(亮度)数据在 planes0,UV 数据在 planes1 和 planes2;③ toBitmap() 非常耗时(涉及格式转换),实时分析不要用;④ setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) 确保只处理最新帧,避免队列堆积;⑤ 分析器运行在 CameraX 内部线程,不能直接操作UI,需切到主线程。

Q5:Camera1 的 setPreviewCallback 和 Camera2 的 ImageReader 接收帧数据有什么区别?

参考答案:

Camera1 的 PreviewCallback 回调的 data 是 NV21/YV12 格式的字节数组 ,直接可用但每次都拷贝一份,GC 压力大。Camera2 用 ImageReader + Surface,数据以 ByteBuffer 形式提供,可以 zero-copy 传递给处理链(ImageReader 直接绑定到 CaptureSession 输出),效率更高。Camera2 的 ImageReader 可以配置多种格式(JPEG/YUV_420_888/Raw),而 Camera1 只能被动接收 Sensor 默认格式。

Q6:Camera2 中如何实现点击屏幕指定位置对焦?

参考答案:

① 获取点击坐标;② 转换为 Camera2 的 MeteringRectangle(以 (0,0)-(1000,1000) 为归一化坐标系);③ 构建 CaptureRequest,设置 CONTROL_AF_REGIONS + CONTROL_AE_REGIONS(曝光也要一起测光);④ 设置 CONTROL_AF_TRIGGERTRIGGER_START_CAMERA 开始对焦;⑤ 对焦完成回调 onAutoFocusFinished 后重新切换回 CONTROL_AF_MODE_CONTINUOUS_PICTURE。CameraX 可直接用 CameraX.letFocusOnPoint() 一行搞定。

Q7:CameraX 的 PreviewView 有哪几种 ScaleType?如何选择?

参考答案:

  • FIT_CENTER:等比缩放,画面完整,多余区域填黑边(预览推荐
  • FILL_CENTER:拉伸填满,画面填满但可能变形(不适合拍远景文字
  • FIT_START/FIT_END:等比缩放,贴合某一边(适合特定布局)
  • CENTER_INSIDE:和 FIT_CENTER 类似但不放大

拍近景文字/文档时建议 FILL_CENTER(防止黑边),但会变形;FIT_CENTER 文字最真实但有黑边。

Q8:在 Android 10(API 29)的 Scoped Storage 限制下,如何保存 CameraX 拍的照片?

参考答案:

三种方案:① MediaStore:用 ContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) 方式创建输出流,跨应用可访问;② 应用私有目录 context.filesDir:无需权限,但只有本应用能访问;③ 声明 requestLegacyExternalStorage="true":仅 Android 10 临时有效,Android 11+ 无效。最推荐方案是 MediaStore,兼容性好且无需存储权限(Android Q+ MediaStore 写入不需要 WRITE_EXTERNAL_STORAGE 权限)。

相关推荐
mmsx2 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp
YF02113 小时前
如何进入Android工程模式
android
Kevin Coding4 小时前
JsonConvert:适用于 Android、鸿蒙与 Flutter 的 JSON 转 Model 插件
android·flutter·harmonyos
xcLeigh6 小时前
Go入门:基本数据类型全览与选择指南
android·服务器·golang·教程·go热门
≮傷£≯√6 小时前
图片合并成视频 img2video ffmpeg
ffmpeg·音视频
alexhilton6 小时前
规范驱动开发:让AI生成符合预期的KMP代码
android·kotlin·android jetpack
码农coding6 小时前
android12 ActivityManagerService开机启动分析
android
A13345558 小时前
视频特效字幕怎么翻译?保姆级AI字幕与外挂字幕教程
人工智能·音视频