8. Camera2拍照功能:从按下快门到照片落盘
拍照功能,说白了就是三个步骤:发起请求 → 捕获数据 → 保存文件。但Camera2把这套流程拆得特别细,刚接触时容易懵。我刚开始做的时候也绕了不少弯路,今天咱们就把这条链路彻底捋清楚。
核心流程速览
创建CaptureRequest → 提交到CaptureSession → ImageReader接收数据 → 回调中处理图片 → 保存到相册
8.1 创建拍照专用的CaptureRequest
拍照和预览用的CaptureRequest不一样。预览追求流畅,拍照追求画质。我个人习惯把拍照请求单独构建,不和预览混在一起。
// 构建拍照请求
CaptureRequest.Builder photoBuilder = cameraDevice
.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
// 设置对焦模式——拍照时用连续自动对焦
photoBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 设置闪光灯——根据场景自动决定
photoBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 设置输出目标——把ImageReader的Surface加进去
photoBuilder.addTarget(imageReader.getSurface());
// 生成最终的CaptureRequest
CaptureRequest photoRequest = photoBuilder.build();
小技巧:TEMPLATE_STILL_CAPTURE 和 TEMPLATE_PREVIEW 的默认参数不同。拍照模板会优先保证画质,预览模板优先保证帧率。别混用,否则照片可能偏暗或偏亮。
8.2 捕获静态图片——capture() vs captureBurst()
提交拍照请求有两种方式:
- capture():单次捕获,拍一张照片。日常拍照够用了。
- captureBurst():连拍模式,适合抓拍运动场景。
我一般用capture(),简单直接。提交代码长这样:
// 在拍照按钮点击时触发
captureSession.capture(photoRequest, new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
// 拍照完成回调——此时ImageReader那边已经在处理图片了
Log.d("Camera", "拍照完成,图片数据已进入ImageReader");
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureFailure failure) {
// 拍照失败——常见原因:快门速度太慢、对焦超时
Log.e("Camera", "拍照失败: " + failure.getReason());
}
}, null);
注意:capture() 是异步的。调用后立刻返回,图片数据通过ImageReader回调回来。不要在onCaptureCompleted里直接拿图片数据——那里只有元数据,没有像素数据。
8.3 ImageReader回调处理——核心中的核心
ImageReader是Camera2里专门用来接收图片数据的组件。它内部维护了一个队列,相机捕获到的每一帧都会放进这个队列。我们通过回调把数据取出来。
初始化ImageReader的代码:
// 创建ImageReader——宽高、格式、最大图片数
ImageReader imageReader = ImageReader.newInstance(
width, height,
ImageFormat.JPEG, // 直接输出JPEG格式
2 // 队列最大深度,2就够了
);
// 设置监听器——图片数据到达时触发
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
// 这里是在Camera线程回调的,别做耗时操作
Image image = reader.acquireLatestImage();
if (image != null) {
// 处理图片——保存到文件
saveImageToFile(image);
// 用完必须关闭,否则内存泄漏
image.close();
}
}
}, backgroundHandler);
为什么会用 acquireLatestImage() 而不是 acquireNextImage()?
嗯,这里有个坑。如果相机帧率很快,而你的保存逻辑比较慢,队列里会堆积多张图片。acquireNextImage() 会按顺序一张张取,容易导致OOM。acquireLatestImage() 则直接丢弃旧帧,只拿最新的那张。我建议你始终用 acquireLatestImage()。
8.4 保存图片到相册——两种主流方式
图片数据拿到了,接下来就是存盘。Android有两种方式:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| MediaStore API | Android 10+ 推荐 | 无需存储权限,系统自动管理 | API较新,低版本需兼容 |
| File API + 广播通知 | 所有版本通用 | 简单直接,控制力强 | 需要WRITE_EXTERNAL_STORAGE权限 |
我个人更推荐MediaStore方式,毕竟Android 10以后强制分区存储,File API越来越受限。下面给出MediaStore的示例:
private void saveImageToFile(Image image) {
// 从Image中取出JPEG字节数据
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 生成文件名——用时间戳避免重名
String filename = "IMG_" + System.currentTimeMillis() + ".jpg";
// 使用MediaStore保存到相册
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/Camera2Demo");
Uri uri = getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try (OutputStream out = getContentResolver().openOutputStream(uri)) {
out.write(bytes);
out.flush();
Log.d("Camera", "图片已保存: " + uri.toString());
} catch (IOException e) {
Log.e("Camera", "保存失败", e);
}
}
避坑指南:我曾经在保存图片时忘记关闭Image对象,结果相机预览卡死、拍照越来越慢。后来排查发现是ImageReader队列满了,新数据进不来。记住:acquireLatestImage() 拿到的Image,用完必须 close(),否则就是给自己挖坑。
8.5 完整调用链——把一切串起来
上面讲了各个零件,现在把它们组装起来。拍照按钮的完整逻辑:
- 用户点击拍照按钮
- 调用
captureSession.capture(photoRequest, callback, handler) - 相机硬件捕获一帧数据,写入ImageReader的Surface
- ImageReader回调
onImageAvailable()被触发 - 在回调中调用
acquireLatestImage()拿到Image对象 - 从Image中提取JPEG字节数据
- 通过MediaStore写入相册
- 关闭Image对象,释放资源
你看,链路其实很清晰。但每个环节都有细节要注意。比如第5步和第8步,漏掉任何一个都会出问题。
核心要点总结
- 拍照用TEMPLATE_STILL_CAPTURE,别用预览模板
- capture()是异步的,图片数据走ImageReader回调
- 始终用acquireLatestImage()避免OOM
- Image用完必须close()
- Android 10+推荐MediaStore保存,兼容性好
好了,拍照功能的核心逻辑就这些。你把这个流程跑通,Camera2拍照这块就算拿下了。下一章咱们聊聊更进阶的话题——不过那是后话了。
公众号:蓝海资料掘金营,微信deep3321