8. 拍照功能实现:配置拍照CaptureRequest、触发拍照Capture、处理照片回调、保存照片到文件

拍照功能,说白了就是让摄像头在某个瞬间“定格”一帧画面。很多初学者觉得这很简单——不就是按个快门吗?但实际做起来,你会发现里面有不少门道。我最早做拍照功能时,就踩过照片方向不对、保存路径权限崩溃这些坑。今天咱们一步步把它理清楚。

8.1 拍照的整体流程

先看一张流程图,帮你快速建立整体认知:

拍照功能核心流程 1. 创建拍照CaptureRequest 2. 触发拍照Capture 3. 处理照片回调 4. 保存照片到文件 关键点 • 使用ImageReader接收数据 • 注意旋转角度修正 • JPEG质量建议95% • 保存路径用MediaStore • 异步处理避免ANR • 记得释放Image对象 • 权限检查不能少

嗯,流程不复杂,但每一步都有细节。下面我们逐个击破。

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 里直接拿照片数据?其实这里只是通知你“拍照动作完成了”,真正的像素数据要通过 ImageReaderOnImageAvailableListener 来获取。

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