3. Camera2拍照

任务说明

实现一个基于 Camera2 API 的拍照功能:点击按钮后捕获单帧静态图片,将 JPEG 数据保存到应用私有目录(context.filesDir),并在界面上显示该图片的缩略图。本案例聚焦于拍照链路的最小完整实现,不涉及视频录制、前后摄切换、变焦等扩展功能。

实现方案

使用 CameraCaptureSession.CaptureCallback 的 onCaptureCompleted 回调获取已捕获的 ImageReader 帧,从中读取 JPEG 字节流写入文件。缩略图通过 BitmapFactory 解码文件后缩放显示在 ImageView 中。核心流程:打开相机 → 创建预览会话 → 触发 capture() → 在回调中保存图片 → 更新 UI。

代码实现

// Camera2PhotoCapture.kt
class Camera2PhotoCapture(private val context: Context) {
    private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
    private var cameraDevice: CameraDevice? = null
    private var captureSession: CameraCaptureSession? = null
    private var imageReader: ImageReader? = null
    private var previewSurface: Surface? = null
    private var cameraId = "0"

    // 保存回调
    var onPhotoSaved: ((File) -> Unit)? = null

    // 打开相机
    fun openCamera() {
        if (checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) return
        cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
            override fun onOpened(camera: CameraDevice) {
                cameraDevice = camera
                createSession()
            }
            override fun onDisconnected(camera: CameraDevice) { camera.close() }
            override fun onError(camera: CameraDevice, error: Int) { camera.close() }
        }, null)
    }

    // 创建捕获会话(预览 + 拍照)
    private fun createSession() {
        val device = cameraDevice ?: return
        val size = cameraManager.getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
            .getOutputSizes(ImageFormat.JPEG)[0]

        imageReader = ImageReader.newInstance(size.width, size.height, ImageFormat.JPEG, 2)
        imageReader?.setOnImageAvailableListener({ reader ->
            val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
            val buffer = image.planes[0].buffer
            val bytes = ByteArray(buffer.remaining())
            buffer.get(bytes)
            image.close()

            // 保存到私有目录
            val file = File(context.filesDir, "photo_${System.currentTimeMillis()}.jpg")
            FileOutputStream(file).use { it.write(bytes) }
            onPhotoSaved?.invoke(file)
        }, null)

        previewSurface = Surface(imageReader!!.surface)
        device.createCaptureSession(listOf(previewSurface!!), object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) {
                captureSession = session
                // 设置预览请求(此处仅用于维持会话,不实际显示预览)
                val request = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
                    addTarget(previewSurface!!)
                }
                session.setRepeatingRequest(request.build(), null, null)
            }
            override fun onConfigureFailed(session: CameraCaptureSession) { /* 可在此提示用户 */ }
        }, null)
    }

    // 触发拍照
    fun takePhoto() {
        val device = cameraDevice ?: return
        val session = captureSession ?: return
        val request = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
            addTarget(previewSurface!!)
        }
        session.capture(request.build(), object : CameraCaptureSession.CaptureCallback() {
            override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
                // JPEG 数据已由 ImageReader 回调处理
            }
        }, null)
    }

    fun close() {
        captureSession?.close()
        cameraDevice?.close()
        imageReader?.close()
    }
}

界面与调用

// MainActivity.kt(关键部分)
class MainActivity : AppCompatActivity() {
    private lateinit var photoCapture: Camera2PhotoCapture
    private lateinit var imageView: ImageView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        imageView = findViewById(R.id.thumbnail)
        photoCapture = Camera2PhotoCapture(this)

        findViewById<Button>(R.id.btn_capture).setOnClickListener {
            photoCapture.takePhoto()
        }

        photoCapture.onPhotoSaved = { file ->
            runOnUiThread {
                val bmp = BitmapFactory.decodeFile(file.absolutePath)
                val scaled = Bitmap.createScaledBitmap(bmp, 200, 200, true)
                imageView.setImageBitmap(scaled)
            }
        }
    }

    override fun onResume() {
        super.onResume()
        if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
            photoCapture.openCamera()
        } else {
            requestPermissions(arrayOf(Manifest.permission.CAMERA), 1)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        photoCapture.close()
    }
}

布局文件

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <ImageView
        android:id="@+id/thumbnail"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#EEE"
        android:contentDescription="缩略图"/>

    <Button
        android:id="@+id/btn_capture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="拍照"
        android:layout_marginTop="20dp"/>
</LinearLayout>

依赖与权限

<!-- AndroidManifest.xml 中添加 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />

// build.gradle (Module) 中无需额外依赖,Camera2 为系统 API

验证方法

  1. 在真机上运行应用(Camera2 不支持模拟器预览)。
  2. 授予相机权限后,点击“拍照”按钮。
  3. 观察 ImageView 是否出现 200×200 的缩略图。
  4. 通过 adb shell run-as com.your.package ls files/ 查看私有目录下是否生成 photo_*.jpg 文件。
  5. 判定标准:缩略图正常显示且文件存在,文件大小大于 10KB(JPEG 有效数据),即视为拍照链路成功。

预期输出:点击拍照后,files/ 目录下新增 JPEG 文件,界面显示对应缩略图。若相机权限未授予或设备无相机,则无输出且应用不崩溃。