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。
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 配置与录制控制。