Android CameraX 相機功能

早期 Android 的相機開發(Camera1 與 Camera2 API)因硬體相容性碎片化而極度難用。CameraX 是 Jetpack 的一部分,旨在解決這些痛點。它不僅易用,還自動處理了各種裝置的生命週期綁定與比例設定問題。

為什麼選擇 CameraX?

  • API 簡潔:將功能封裝為「使用案例 (UseCases)」,程式碼量大幅減少。
  • 相容性檢查:自動處理超過 98% Android 裝置的特殊行為(如預覽旋轉角度)。
  • 生命週期感知:自動根據 LifecycleOwner(如 Activity/Fragment)啟動或停止相機,防止記憶體洩漏。

環境配置

build.gradle.kts 中加入 CameraX 核心組件:

val cameraxVersion = "1.3.4"
dependencies {
    implementation("androidx.camera:camera-core:$cameraxVersion")
    implementation("androidx.camera:camera-camera2:$cameraxVersion")
    implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
    implementation("androidx.camera:camera-view:$cameraxVersion")
    implementation("androidx.camera:camera-video:$cameraxVersion")
}

確保 AndroidManifest.xml 加入權限與關鍵硬體宣告:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" />

核心使用案例 (UseCases)

CameraX 的核心思想是將相機功能拆分為不同的 UseCase,你可以同時綁定一個或多個。

預覽 (Preview)

在 Compose 中,我們需要將傳統的 PreviewView 置入 AndroidView 中。

@Composable
fun CameraPreviewScreen() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val previewView = remember { PreviewView(context) }

    LaunchedEffect(Unit) {
        val cameraProvider = ProcessCameraProvider.getInstance(context).get()
        
        val preview = Preview.Builder().build().also {
            it.setSurfaceProvider(previewView.surfaceProvider)
        }

        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
        } catch (e: Exception) {
            Log.e("CameraX", "綁定失敗", e)
        }
    }

    AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
}

拍照 (ImageCapture)

ImageCapture 負責處理高畫質靜態影像的獲取。

val imageCapture = remember { ImageCapture.Builder().build() }

// 在呼叫 bindToLifecycle 時加入它
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)

// 執行拍照並存到媒體庫
fun takePhoto(context: Context) {
    val name = "IMG_${System.currentTimeMillis()}.jpg"
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
    }

    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        .build()

    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(context),
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                // 拍照成功!
            }
            override fun onError(exc: ImageCaptureException) {
                // 拍照失敗
            }
        }
    )
}

影像分析 (ImageAnalysis)

用於即時取得相機畫面 Buffer 進行分析,如 QR Code 辨識或人臉檢測。

val analysisUseCase = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

analysisUseCase.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
    // imageProxy.image 提供原始 Buffer
    // 處理完成後務必釋放,否則會阻塞下一格畫面
    imageProxy.close()
}

相機控制 (Camera Control)

一旦相機綁定成功,你可以透過 Camera 實例來控制硬體行為。

val camera = cameraProvider.bindToLifecycle(...)
val cameraControl = camera.cameraControl
val cameraInfo = camera.cameraInfo

// 1. 控制縮放 (Zoom)
cameraControl.setZoomRatio(2.0f)

// 2. 開啟閃光燈 (Torch)
cameraControl.enableTorch(true)

// 3. 點擊對焦 (Tap to Focus)
fun focusOnPoint(x: Float, y: Float) {
    val factory = previewView.meteringPointFactory
    val point = factory.createPoint(x, y)
    val action = FocusMeteringAction.Builder(point).build()
    cameraControl.startFocusAndMetering(action)
}

總結

CameraX 透過 UseCase 模型簡化了各種相機工作流。建議開發者總是結合 Coroutines 處理圖片保存與分析結果,並確保遵循最佳實踐:在 bindToLifecycle 之前先呼叫 unbindAll()