3. 拍照功能实现:配置 ImageCapture 用例、拍照回调、保存图片到相册

3.1 本章概述

拍照是相机应用最核心的功能之一。CameraX 通过 ImageCapture 用例提供标准化拍照能力,开发者只需完成“配置用例 → 绑定生命周期 → 触发拍照 → 处理回调 → 保存图片”五个步骤。本章从原理出发,逐步拆解每一环节的 API 调用、参数配置、回调处理以及保存策略,同时覆盖调试方法与常见陷阱。

前置知识:已了解 CameraX 的 ProcessCameraProvider 生命周期绑定机制(第 2 章)。拍照用例在 bindToLifecycle 时与其他用例(Preview / ImageAnalysis)共存于同一配置列表中。

3.2 原理:ImageCapture 用例工作模型

ImageCapture 内部封装了 Camera2 的 CaptureRequest 与 CaptureCallback 的复杂交互。其核心处理链路如下:

  • 触发拍照:调用 takePicture() 方法,CameraX 内部创建一个单次捕获请求。
  • 图像捕获:底层 Camera2 返回 JPEG 或 YUV_420_888 格式的原始帧。
  • 回调分发:数据进入 OnImageCapturedCallback(内存模式)或 OnImageSavedCallback(文件模式)。
  • 后处理:Exif 写入(方向、GPS)、缩略图生成、存储到 MediaStore。
ImageCapture 拍照流程 应用调用 takePicture() CameraX 捕获请求 Camera2 返回 JPEG 帧 回调方式:内存 / 文件 OnImageCapturedCallback OnImageSavedCallback 写入 MediaStore / 文件 同一线程,顺序执行

3.3 配置 ImageCapture 用例

通过 ImageCapture.Builder 设置输出格式、分辨率、闪关灯模式等参数,再通过 bindToLifecycle 注册到 CameraX 会话。

3.3.1 核心配置参数

参数方法选项 / 取值说明
setCaptureMode()CAPTURE_MODE_MINIMIZE_LATENCY / CAPTURE_MODE_MAXIMIZE_QUALITY延迟优先或画质优先
setFlashMode()FLASH_MODE_AUTO / FLASH_MODE_ON / FLASH_MODE_OFF闪光灯行为控制
setTargetResolution()Size 对象(如 1920x1080)输出图片分辨率(宽高比自动匹配屏幕)
setTargetRotation()Surface.ROTATION_0/90/180/270输出图片方向,通常由显示屏方向决定
setJpegQuality()0 ~ 100(int)JPEG 压缩质量,默认 100
// 典型配置示例
val imageCapture = ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
    .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
    .setTargetResolution(Size(1920, 1080))
    .setTargetRotation(Surface.ROTATION_0)
    .setJpegQuality(95)
    .build()
实际建议:分辨率不要盲目选最大。setTargetResolution 会限制输出边长,但最终分辨率由相机传感器最佳匹配决定。通常 1920x1080 兼顾清晰度与存储成本。

3.3.2 绑定到生命周期

val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
    val cameraProvider = cameraProviderFuture.get()
    val preview = Preview.Builder().build()
    val imageCapture = ImageCapture.Builder().build()
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    val camera = cameraProvider.bindToLifecycle(
        lifecycleOwner,
        cameraSelector,
        preview,
        imageCapture
    )
}, ContextCompat.getMainExecutor(context))
常见陷阱:bindToLifecycle 一旦调用,所有用例必须非空。如果只需要拍照不需要预览,仍需要传入一个 Preview 对象(预览 Surface 可以设置一个空的 SurfaceProvider),否则 CameraX 可能抛出异常。

3.4 拍照回调:两种模式对比

模式回调接口适用场景数据形式
内存模式OnImageCapturedCallback需要直接在 App 内处理图像(如滤镜、裁剪、AI 分析)ImageProxy(ByteBuffer / YUV_420_888)
文件模式OnImageSavedCallback直接保存到磁盘或相册,不关心中间数据OutputFileResults(保存路径)

3.4.1 内存模式回调

imageCapture.takePicture(
    ContextCompat.getMainExecutor(context),
    object : ImageCapture.OnImageCapturedCallback() {
        override fun onCaptureSuccess(imageProxy: ImageProxy) {
            // imageProxy.image 可获取 ByteBuffer
            // 注意:必须调用 imageProxy.close() 释放帧
            imageProxy.close()
        }

        override fun onError(exception: ImageCaptureException) {
            Log.e("CameraX", "拍照失败: ${exception.message}")
        }
    }
)

3.4.2 文件模式回调

val photoFile = File(
    context.getExternalFilesDir(Environment.DIRECTORY_PICTURES),
    "IMG_${System.currentTimeMillis()}.jpg"
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

imageCapture.takePicture(
    outputOptions,
    ContextCompat.getMainExecutor(context),
    object : ImageCapture.OnImageSavedCallback() {
        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
            Log.d("CameraX", "图片已保存: ${photoFile.absolutePath}")
        }

        override fun onError(exception: ImageCaptureException) {
            Log.e("CameraX", "保存失败: ${exception.message}")
        }
    }
)
选择建议:大多数拍照 App 使用 文件模式 更简单,避免自己处理 JPEG 编码与 Exif。如果需要实时预览拍照效果或添加滤镜,则选用内存模式。

3.5 保存图片到相册

Android 10(API 29)之后推荐使用 MediaStore 保存在系统相册,应用卸载后图片仍保留。Android 10 以下使用 Environment.getExternalStoragePublicDirectory() 并扫描 MediaScanner。

3.5.1 MediaStore 保存(API 29+)

val contentValues = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/CameraXDemo")
}

val uri = context.contentResolver.insert(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    contentValues
)

// 将文件模式输出指向 MediaStore URI
val outputOptions = ImageCapture.OutputFileOptions.Builder(
    context.contentResolver,
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    contentValues
).build()

3.5.2 传统文件保存(兼容旧版本)

val photoFile = File(
    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
    "CameraXDemo/IMG_${System.currentTimeMillis()}.jpg"
)
photoFile.parentFile?.mkdirs()

// 写文件后再通知 MediaScanner
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
MediaScannerConnection.scanFile(context, arrayOf(photoFile.absolutePath), null, null)

3.6 完整代码示例:拍照并保存到相册

class CameraCaptureFragment : Fragment() {
    private var imageCapture: ImageCapture? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val preview = Preview.Builder().build()
        imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
            .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
            .build()

        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            cameraProvider.bindToLifecycle(
                viewLifecycleOwner,
                CameraSelector.DEFAULT_BACK_CAMERA,
                preview,
                imageCapture
            )
        }, ContextCompat.getMainExecutor(requireContext()))

        // 拍照按钮点击
        view.findViewById<Button>(R.id.btn_capture).setOnClickListener {
            takePhoto()
        }
    }

    private fun takePhoto() {
        val imageCapture = imageCapture ?: return
        val contentValues = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/CameraXDemo")
        }
        val outputOptions = ImageCapture.OutputFileOptions.Builder(
            requireContext().contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ).build()

        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(requireContext()),
            object : ImageCapture.OnImageSavedCallback() {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    Toast.makeText(requireContext(), "拍照成功", Toast.LENGTH_SHORT).show()
                }

                override fun onError(exception: ImageCaptureException) {
                    Toast.makeText(requireContext(), "拍照失败: ${exception.message}", Toast.LENGTH_LONG).show()
                }
            }
        )
    }
}

3.7 调试与常见错误

错误现象可能原因解决方案
拍照后回调无响应主线程阻塞 / 未在 MainExecutor 执行确认 takePicture 使用 ContextCompat.getMainExecutor()
图片保存后相册不显示MediaStore RELATIVE_PATH 不正确 / 未扫描检查路径格式;Android 10 以下手动触发 MediaScanner
Flash 模式不生效当前场景不支持 / 前置摄像头无闪光灯调用前检查 CameraInfo.flashModeAvailable()
拍照尺寸与预览不一致setTargetResolution 宽高比与 Preview 不匹配使用 AspectRatio.RATIO_4_3 或 RATIO_16_9 统一比例
内存模式 ImageProxy 泄漏未及时调用 close()使用 use { } 扩展函数自动释放
性能关键点:拍照是 I/O 密集型操作。连续快速拍照时,CameraX 内部维护一个队列,如果在 onImageSaved 中做大量耗时操作(如网络上传),会阻塞后续拍照。建议拍照后采用 WorkManager 或 协程 异步处理存储与后处理。

3.8 稳定性与优化建议

  • 释放 ImageProxy:内存模式下务必在 onCaptureSuccess 中调用 imageProxy.close(),否则 CameraX 无法推送下一帧,导致拍照卡死。
  • 避免拍照抖动:设置 setTargetRotation 为当前显示屏旋转角度,避免图片方向错误导致后期旋转开销。
  • 节约存储:setJpegQuality(85) 可以在肉眼无损前提下减少 30%~50% 文件大小。
  • 异常捕获:在 onError 中区分 ImageCaptureException 的错误码(如 ERROR_CAMERA_CLOSED、ERROR_FILE_IO),针对性恢复。
  • 后台处理:文件模式的保存路径使用 ContentResolver 配合 MediaStore 时,大文件写入可能会触发 ANR。建议在 takePicture 后切换到后台协程做 Exif 写入或缩略图生成。
本章小结:ImageCapture 是 CameraX 三大用例中最直观的上手点。通过 Builder 配置参数、选择内存/文件回调、利用 MediaStore 写入相册,即可实现一个生产级拍照功能。下一章将深入 视频录制,涉及 VideoCapture 用例、MediaRecorder 配置与录制控制。