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 完整拍照流程总结
- 创建 ImageReader:指定 JPEG 格式、推荐尺寸、最大图片数。
- 设置监听器:在后台线程中处理
onImageAvailable回调。 - 构建 CaptureRequest:同时添加预览 Surface 和 ImageReader Surface。
- 触发拍照:调用
capture()方法,传入请求和回调。 - 保存图片:从
Image中读取字节,写入文件并通知媒体库。 - 资源释放:在 Activity/Fragment 销毁时关闭 ImageReader。
注意事项:
-ImageReader的回调在后台线程执行,不要在此线程中直接更新 UI。
- 拍照完成后,如果不再需要,可以调用imageReader.close()释放资源。
- 对于高分辨率图片,建议在子线程中完成文件写入,避免阻塞主线程。