4、Camera2 拍照实战:配置 ImageReader、捕获静态图片、保存到相册

在上一节中,我们完成了 Camera2 的预览流程。本节将在此基础上,实现拍照功能的核心:通过 ImageReader 捕获静态图片,并将其保存到系统相册。拍照流程与预览共享同一个 CameraCaptureSession,但需要额外的配置来处理 JPEG 数据。

4.1 ImageReader 的配置与创建

ImageReader 是 Camera2 中用于接收图像数据(如 JPEG、YUV)的关键类。拍照时,我们需要创建一个专门用于接收 JPEG 数据的 ImageReader

4.1.1 关键参数

  • width / height:通常使用 CameraCharacteristics 获取的 StreamConfigurationMap 中推荐的 JPEG 输出尺寸,而非预览尺寸。
  • format:固定为 ImageFormat.JPEG
  • maxImages:建议设置为 1 或 2,避免内存占用过高。

4.1.2 代码实现

// 获取相机支持的最大 JPEG 尺寸
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val jpegSizes = map?.getOutputSizes(ImageFormat.JPEG)
val jpegSize = jpegSizes?.maxByOrNull { it.width * it.height } ?: Size(1920, 1080)

// 创建 ImageReader
val imageReader = ImageReader.newInstance(
    jpegSize.width,
    jpegSize.height,
    ImageFormat.JPEG,
    1 // maxImages
)

// 设置 OnImageAvailableListener,在子线程中处理图片
imageReader.setOnImageAvailableListener(
    { reader ->
        val image = reader?.acquireLatestImage()
        image?.let { saveImageToGallery(it) }
    },
    backgroundHandler // 后台 HandlerThread 的 Handler
)

4.2 配置拍照的 CaptureRequest

拍照时,我们需要构建一个 CaptureRequest,并将 ImageReader 的 Surface 作为输出目标。注意:预览 Surface 和 ImageReader Surface 可以同时存在于同一个 Session 中

4.2.1 构建拍照请求

// 假设已有 previewSurface(来自 TextureView/SurfaceView)
val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
    .apply {
        addTarget(previewSurface)      // 保持预览
        addTarget(imageReader.surface) // 添加拍照输出目标

        // 设置自动对焦模式
        if (characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.contains(
                CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE
            ) == true) {
            set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
        }

        // 设置自动曝光
        set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)

        // 设置 JPEG 质量(0-100)
        set(CaptureRequest.JPEG_QUALITY, 95.toByte())

        // 可选:设置旋转角度,确保图片方向正确
        set(CaptureRequest.JPEG_ORIENTATION, getOrientation())
    }

4.2.2 触发拍照

// 在拍照按钮点击事件中调用
captureSession?.capture(captureRequestBuilder.build(), object : CameraCaptureSession.CaptureCallback() {
    override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
        super.onCaptureStarted(session, request, timestamp, frameNumber)
        // 拍照开始,可更新 UI(如显示快门动画)
    }

    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
        super.onCaptureCompleted(session, request, result)
        // 拍照完成,ImageReader 的回调会异步处理图片
    }
}, backgroundHandler)

4.3 保存图片到相册

ImageReader.OnImageAvailableListener 中,我们获取到 JPEG 数据后,需要将其写入文件并通知系统媒体库。

4.3.1 从 Image 中提取字节数据

private fun saveImageToGallery(image: Image) {
    // 注意:Image 对象必须在使用后 close()
    image.use { img ->
        val buffer = img.planes[0].buffer
        val bytes = ByteArray(buffer.remaining())
        buffer.get(bytes)

        // 生成文件名
        val filename = "IMG_${System.currentTimeMillis()}.jpg"
        val outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
        if (!outputDir.exists()) outputDir.mkdirs()
        val file = File(outputDir, filename)

        // 写入文件
        FileOutputStream(file).use { fos ->
            fos.write(bytes)
            fos.flush()
        }

        // 通知系统相册刷新
        val values = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, filename)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
            put(MediaStore.Images.Media.IS_PENDING, 1) // Android 10+ 使用
        }

        val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        uri?.let {
            context.contentResolver.openOutputStream(it)?.use { outputStream ->
                outputStream.write(bytes)
            }
            values.clear()
            values.put(MediaStore.Images.Media.IS_PENDING, 0)
            context.contentResolver.update(it, values, null, null)
        }
    }
}

4.3.2 兼容性说明

Android 版本 推荐保存方式
Android 9 及以下 直接写入 Environment.getExternalStoragePublicDirectory,然后通过 MediaScannerConnection 扫描
Android 10+ 使用 MediaStore API,设置 IS_PENDING 标志确保原子性

4.4 获取正确的图片方向

由于手机传感器方向与屏幕方向可能不一致,拍照时需要根据设备旋转角度设置 JPEG_ORIENTATION,否则保存的图片可能旋转 90° 或 270°。

private fun getOrientation(): Int {
    val rotation = windowManager.defaultDisplay.rotation
    val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0

    return when (rotation) {
        Surface.ROTATION_0 -> sensorOrientation
        Surface.ROTATION_90 -> (sensorOrientation + 90) % 360
        Surface.ROTATION_180 -> (sensorOrientation + 180) % 360
        Surface.ROTATION_270 -> (sensorOrientation + 270) % 360
        else -> sensorOrientation
    }
}

4.5 完整拍照流程总结

  1. 创建 ImageReader:指定 JPEG 格式、推荐尺寸、最大图片数。
  2. 设置监听器:在后台线程中处理 onImageAvailable 回调。
  3. 构建 CaptureRequest:同时添加预览 Surface 和 ImageReader Surface。
  4. 触发拍照:调用 capture() 方法,传入请求和回调。
  5. 保存图片:从 Image 中读取字节,写入文件并通知媒体库。
  6. 资源释放:在 Activity/Fragment 销毁时关闭 ImageReader。

注意事项
- ImageReader 的回调在后台线程执行,不要在此线程中直接更新 UI。
- 拍照完成后,如果不再需要,可以调用 imageReader.close() 释放资源。
- 对于高分辨率图片,建议在子线程中完成文件写入,避免阻塞主线程。