8. 拍照功能实现:配置拍照CaptureRequest、触发拍照Capture、处理照片回调、保存照片到文件
拍照功能,说白了就是让摄像头在某个瞬间“定格”一帧画面。很多初学者觉得这很简单——不就是按个快门吗?但实际做起来,你会发现里面有不少门道。我最早做拍照功能时,就踩过照片方向不对、保存路径权限崩溃这些坑。今天咱们一步步把它理清楚。
8.1 拍照的整体流程
先看一张流程图,帮你快速建立整体认知:
嗯,流程不复杂,但每一步都有细节。下面我们逐个击破。
8.2 配置拍照CaptureRequest
拍照的请求和预览的请求不太一样。预览我们追求流畅,拍照我们追求画质。所以需要单独构建一个 CaptureRequest。
核心思路:基于预览的Session创建一个新的拍照请求,设置JPEG格式、质量、旋转角度等参数。
// 构建拍照请求
val captureRequestBuilder = cameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
)
// 绑定目标Surface——这里用ImageReader的Surface
captureRequestBuilder.addTarget(imageReader.surface)
// 设置JPEG质量
captureRequestBuilder.set(
CaptureRequest.JPEG_QUALITY,
95 // 我个人习惯用95,平衡画质和文件大小
)
// 设置照片旋转角度
val rotation = getOrientation(sensorOrientation, displayRotation)
captureRequestBuilder.set(
CaptureRequest.JPEG_ORIENTATION,
rotation
)
// 可选:设置自动对焦模式
captureRequestBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
// 可选:设置闪光灯模式
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
)
💡 小技巧:旋转角度这块容易出错。我建议写一个工具方法,根据传感器方向和屏幕方向计算出正确的JPEG方向值。否则拍出来的照片可能是横着的。
8.3 触发拍照Capture
请求构建好了,怎么触发呢?调用 captureSession.capture() 方法即可。注意,这里要传入一个 CaptureCallback 来监听拍照进度。
// 触发拍照
captureSession.capture(
captureRequestBuilder.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long
) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
// 拍照开始了,可以在这里显示快门动画
runOnUiThread {
// 比如闪一下白屏
}
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
// 拍照完成,照片数据会通过ImageReader回调返回
// 这里可以恢复预览
}
},
null
)
你可能会问:为什么不在 onCaptureCompleted 里直接拿照片数据?其实这里只是通知你“拍照动作完成了”,真正的像素数据要通过 ImageReader 的 OnImageAvailableListener 来获取。
8.4 处理照片回调
这是最核心的一步。我们需要监听 ImageReader 的回调,从 Image 对象中取出JPEG字节数据。
// 创建ImageReader
val imageReader = ImageReader.newInstance(
width, height,
ImageFormat.JPEG,
1 // 最大图片数量,拍照一般设为1
)
// 设置监听器
imageReader.setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage()
image?.let {
// 获取JPEG数据
val buffer = it.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
// 重要:用完必须关闭,否则会卡死
it.close()
// 保存照片到文件(异步执行)
savePhotoToFile(bytes)
}
}, backgroundHandler)
⚠️ 注意:Image对象用完后一定要调用 close() 释放。我曾经因为忘记关闭,导致拍照几次后就再也拍不了——ImageReader内部缓冲区被占满了。这个坑我印象很深。
8.5 保存照片到文件
拿到字节数组后,保存就简单了。但要注意两点:一是路径选择,二是权限处理。
Android 10(API 29)之后,推荐使用 MediaStore 保存照片,这样不需要 WRITE_EXTERNAL_STORAGE 权限。如果是低版本,可以用传统文件方式。
private fun savePhotoToFile(bytes: ByteArray) {
// 生成文件名
val filename = "IMG_${System.currentTimeMillis()}.jpg"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 使用 MediaStore
val contentValues = 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 + "/CameraDemo")
}
val uri = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
uri?.let {
contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(bytes)
outputStream.flush()
}
}
} else {
// 低版本直接写文件
val dir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)
val file = File(dir, filename)
FileOutputStream(file).use { outputStream ->
outputStream.write(bytes)
outputStream.flush()
}
}
// 通知相册刷新
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
intent.data = Uri.fromFile(File(path))
sendBroadcast(intent)
}
💡 经验之谈:保存操作一定要放在子线程。我见过有人直接在ImageReader回调里写文件,结果主线程卡顿导致ANR。用 HandlerThread 或者协程都可以。
8.6 完整代码整合
把上面几块拼起来,一个完整的拍照功能就成型了。这里给一个精简的调用示例:
// 1. 初始化ImageReader(在打开Camera之前)
val imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1)
imageReader.setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
val buffer = image.planes[0].buffer
val data = ByteArray(buffer.remaining())
buffer.get(data)
image.close()
// 异步保存
savePhotoToFile(data)
}, backgroundHandler)
// 2. 拍照按钮点击时
btnCapture.setOnClickListener {
val request = cameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
).apply {
addTarget(imageReader.surface)
set(CaptureRequest.JPEG_QUALITY, 95)
set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation())
}
captureSession.capture(request.build(), captureCallback, null)
}
8.7 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 照片方向不对 | 没有设置JPEG_ORIENTATION | 根据传感器方向和屏幕方向计算旋转角度 |
| 拍照后预览卡住 | 没有重新发起预览请求 | 在onCaptureCompleted中重新设置预览 |
| 拍几次后无法拍照 | Image没有close() | 确保每次acquire后都close |
| 保存失败 | 权限不足或路径错误 | 检查权限,Android 10+用MediaStore |
| 照片模糊 | 对焦未完成就拍照 | 设置AF_MODE或等待对焦锁定 |
总结一下:拍照功能说白了就是“构建请求→触发→拿数据→存文件”四步。每一步都有坑,但只要你按照上面的流程来,基本不会出大问题。我个人建议先把基础流程跑通,再考虑添加连拍、HDR、人像模式等高级功能。
公众号:蓝海资料掘金营,微信deep3321