CameraXとそのCopmose対応について


この記事では主にCameraXのCompose対応について触れますが、CameraXに触れたことのない方で、Compose化されたのを機にこれから使ってみようという方も対象に書こうと思います。

ただ、CameraXの概要は簡単に触れるのみで、詳しくは公式のドキュメントを精読いただくことを推奨します。

[CameraX の概要 Android Developers](https://developer.android.com/media/camera/camerax?hl=ja)


## CameraXとCompose対応について

2024年末の[Android Developer Blog](https://android-developers.googleblog.com/2024/12/whats-new-in-camerax-140-and-jetpack-compose-support.html)にて、CameraXのComposeサポートがアナウンスされました。1.5.0-alphaバージョンから使えるようになるということです。

単に端末のカメラで撮影して画像を使うだけなら`MediaStore.ACTION_IMAGE_CAPTURE`を使ったIntentを投げるで十分です。しかし、これは端末のカメラアプリのUIや機能に依存し、本格的にカスタマイズできません。本格的なカメラ機能を提供したい場合はCameraXが向きます。CameraXを使えばUIや機能をカスタマイズできますし、機種依存せず統一的なUIを提供できる上、画像解析なども利用できます。

今までCameraXは従来のAndroid Viewのみ提供されており、Composeのアプリで使うときに課題がありました。またサンプルもAndroid Viewから使う例が多かったと思います。

しかし今回Compose対応になったことでComposeベースのアプリからも扱いやすくなり、複雑なUI要件も比較的容易に実現しやすくなったということです。下記の公式ガイドの[part2の記事](https://medium.com/androiddevelopers/tap-to-focus-mastering-camerax-transformations-in-jetpack-compose-440853280a6e)では、タップしてフォーカスしたときにクラッカーのようなエフェクトを出現させています。

<img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*-fk6tbfnhihneV5i" alt="comeraX" width ="600">

まだ出たばかりなのでドキュメントもこれから増えていくかもしれませんが、すでに公式のMediumの記事もあり、今からでも手を付けやすそうです。

[Create a spotlight effect with CameraX and Jetpack Compose \| by Jolanda Verhoef \| Android Developers \| Jan, 2025 \| Medium](https://medium.com/androiddevelopers/create-a-spotlight-effect-with-camerax-and-jetpack-compose-8a7fa5b76641)

こちらの記事がとてもわかり易いです。この記事の執筆時点ではpart3まで公開されています。

[part2](https://medium.com/androiddevelopers/tap-to-focus-mastering-camerax-transformations-in-jetpack-compose-440853280a6e)
[part3](https://medium.com/androiddevelopers/create-a-spotlight-effect-with-camerax-and-jetpack-compose-8a7fa5b76641)

[google/jetpack\-camera\-app](https://github.com/google/jetpack-camera-app)にgithubリポジトリのサンプルがあり、なかなかリッチになっていますが、実装の参考にできると思います。

従来Viewのサンプルは[android/camera\-samples](https://github.com/android/camera-samples/)にあります。

## CameraXの基本のアーキテキチャ

前述の[CameraX の概要 Android Developers](https://developer.android.com/media/camera/camerax?hl=ja)に詳細は書いてありますが、簡単にキーワードに触れていきます。

### ユースケース

ユースケースとはCameraXを使用する際の機能というかタスクのような概念です。
プレビュー(Preview)、画像解析(ImageAnalytics)、画像キャプチャ(ImageCapture)、動画キャプチャがドキュメントでは紹介されています。(他にもStreamSharingというのがあります)

ユースケースは複数同時に動作させることが可能です。`ProcessCameraProvider.bindToLifecycle`は、ユースケースをvarargで受け取ります。

ユースケースは`Usecaseクラス.Builder().build()`のようなビルダーパターンで作ります。ビルダーを作る拡張関数を作っていてもよいでしょう。

### プレビュー表示とSurfaceRequest

従来Viewの実装でプレビューを表示するためには、PreviewViewを使っていました。
PreviewViewは内部でSurfaceView/TextureViewを使っています。

SurfaceViewは常に更新されるようなものに使われるViewで、TextureViewとSurfaceViewを比較すると後者のほうが効率はよいとのことです。
これらにアクセスするためにSurafaceRequestが必要になります。

CameraXのComposeでは、プレビューを表示するために`CameraXViewfinder`というComposableを用います。
`CameraXViewfinder`の内部を見ていくとSurfaceView/TextureViewが使われていました。
`CameraXViewfinder`を表示するためにSurfaceRequestが必要になります。

surfaceRequestは、`Preview`のユースケースの`setSurfaceProvider`のコールバックから得ることができます。

### CameraController

公式のドキュメントはより簡単に実装できるCameraControllerが紹介されています(従来Viewでの例)。
comopseのサンプルではCameraProviderを使用した例しか見当たらなかったです(現時点ではCameraControllerは、自分が見たところComposeでは使えないと思いました)。
本記事ではCameraProviderを扱います。

### CameraProvider

CameraControllerよりもカスタマイズができるとのこと。
実装ではProcessCameraProviderが出てきます。

### ProcessCameraProviderとライフサイクル

`ProcessCameraProvider.bindToLifecycle`はユースケースだけでなくLifeCycleOwnerを受け取ります。
カメラのライフサイクルをアプリケーションのサイクルにバインドする必要があります。
そうすることでアプリケーションのライフサイクルに合わせた処理を自動で行ってくれるようです。

`bindToLifecycle`でユースケースがバインドされますが、必要なくなったときやユースケースを切り替えるときには、`unbindAll()`でユースケースをアンバインドしなくてはなりません。

### Preview

プレビューのためのユースケースです。
`Preview.Builder().build()`でインスタンスを作ります。

### ImageCapture

画像をキャプチャする際のユースケースです。
`ProcessCameraProvider.bindToLifecycle`に入れる必要があります。
`ImageCapture.takePicture()`することで撮影ができます。

### CameraSelector

必要なカメラを選択します。要は基本的にフロントかバックカメラです。

## 基本の実装

いろいろな概念が登場しましたが、SurfaceRequestと、ProcessCameraProvider.bindToLifecycle()、CameraXViewfinderが鍵になります。
では最低限のコードを実装していきます。いろいろな考慮が含まれておりませんが、動作確認用の最低限のコードということでご了承ください。

また以下にサンプルを公開しています。こっちを見たほうがわかりやすいかもしれません。

[hiroaki404/CameraXComposeSample](https://github.com/hiroaki404/CameraXComposeSample)

まずユースケースをライフサイクルにバインドするメソッドを作ります。
とりあえずここでは最低限のユースケースを作っておきます。

MediumのドキュメントにはロジックをviewModelに切り出すべき、とあるので、ViewModelを使って実装します。

UI側から呼び出すbindToCameraメソッドを作ります。これはcontextとlifecycleOwnerを受け取り、ProcessCameraProviderから、ユースケースをバインドします。


```kotlin
class PreviewViewModel : ViewModel() {
    private val cameraPreviewUseCase = Preview.Builder().build()

    private val takePictureUseCase = ImageCapture.Builder().build()

    suspend fun bindToCamera(context: Context, lifecycleOwner: LifecycleOwner) {
        val processCameraProvider = ProcessCameraProvider.awaitInstance(context)
        processCameraProvider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            cameraPreviewUseCase,
            takePictureUseCase,
        )
        try {
            awaitCancellation()
        } finally {
            processCameraProvider.unbindAll()
        }
    }
}
```

`bindToCamera`関数はsuspend関数で、呼び出し元のコルーチンがキャンセルされたときに、unbindAll()を呼び出すようになっています。

これだけではsurfaceRequestが取得できないので、プレビューのユースケースから取得できるようにし、
UI側にはStateFlowで公開します。

```kotlin
class PreviewViewModel : ViewModel() {
    private var _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
    val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest.asStateFlow()

    private val cameraPreviewUseCase = Preview.Builder().build().apply {
        setSurfaceProvider { newSurfaceRequest ->
            _surfaceRequest.update { newSurfaceRequest }
        }
    }

    // 略
}
```

これでプレビューの準備は完了ですが、撮影もできるようにtakePictureメソッドも置きましょう。
保存先はcacheDirにしているので、撮影画像はAndroid StudioのDevice Explorer等から確認してください。

(例をできるだけ簡単にするためにcacheDirにしてみましたが、共有ストレージへの保存でもよいです)

```kotlin
class PreviewViewModel : ViewModel() {
    // 略

    @OptIn(ExperimentalGetImage::class)
    fun takePicture(context: Context) {
        viewModelScope.launch {
            val timestamp = Instant.now().epochSecond
            val file = File(context.cacheDir, "$timestamp.jpg")
            val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
                file,
            ).build()

            takePictureUseCase.takePicture(
                outputFileOptions = outputFileOptions,
            )
        }
    }
}
```

ではUI側を作ります。ComposeのPreviewViewfinderを使います。
撮影ボタンなどは普通のComposeを使った開発と同じように配置していけば良いので楽です。

```kotlin
@Composable
fun PreviewView(modifier: Modifier = Modifier, viewModel: PreviewViewModel) {
    val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()

    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    LaunchedEffect(lifecycleOwner) {
        viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
    }

    surfaceRequest?.let {
        Box(contentAlignment = Alignment.BottomCenter) {
            CameraXViewfinder(
                modifier = modifier.fillMaxSize(),
                surfaceRequest = it,
            )

            Button(
                modifier = Modifier.navigationBarsPadding(),
                onClick = { viewModel.takePicture(context) },
            ) {
                Text("Take Picture")
            }
        }
    }
}

```

以上です。Composeのアプリでも使いやすくなりましたね。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1165483/77e5adf2-bdd0-41d8-91cb-dc523b5b7864.png" alt="CameraX" width="300">

## おまけ：コードの中を見てみる

CameraXViewfinderの内部はどうなっているのでしょうか？

CameraXViewfinderは引数に`implementationMode`を取ります。これはExternalとEmbeddedがあり、それぞれSurfaceViewとTextureViewを使います。

```kotlin
@Composable
public fun CameraXViewfinder(
    surfaceRequest: SurfaceRequest,
    modifier: Modifier = Modifier,
    implementationMode: ImplementationMode = ImplementationMode.EXTERNAL,
    coordinateTransformer: MutableCoordinateTransformer? = null
) {
// 略
```

前者は最終的には`androidx.compose.foundation`の`AndroidExternalSurface`、後者は`AndroidEmbeddedExternalSurface`を使っているようです。この引数はデフォルトでExternalになっており、SurfaceViewを使ってほしいことがわかります。前者のほうがパフォーマンスに優れているとのことです。`AndroidExternalSurface`は結局`AndroidView`のComposableの内部で`SurfaceView`をラップしています。

SurfaceView推奨なので基本こちらを使えばよいですが、なぜTextureViewをサポートしているのでしょうか？`ImplementationMode`のkDocを見ると、TextureViewではより幅広いデバイスをサポートしている旨が書かれています。SurfaceViewに対応していない端末を対象としているならTextureViewを使うとよいかもしれません。

```kotlin
/**
 * The implementation mode of a Viewfinder.
 *
 * User preference on how the viewfinder should render the viewfinder. The viewfinder is displayed
 * with either a SurfaceView/AndroidExternalSurface or a TextureView/AndroidEmbeddedExternalSurface.
 * - [EXTERNAL] uses a SurfaceView/AndroidExternalSurface, it is generally better when it comes to
 *   certain key metrics, including power and latency.
 * - [EMBEDDED] uses a TextureView/AndroidEmbeddedExternalSurface it is better supported by a wider
 *   range of devices.
 *
 * The option is used to decide what is the best internal implementation given the device
 * capabilities and user configurations.
 */
enum class ImplementationMode(private val id: Int) {
// 略
```

SurfaceViewには以下の特徴があり、下記特徴からカメラのプレビューに使っているようです。
- 従来Viewはメインスレッドのみで描画されるが、SurfaceViewは別スレッドでの描画も可能
- 直接ピクセル単位の描画や滑らかなアニメーション、リアルタイムな描画が可能
- 常に更新されるようなビューに最適（カメラのプレビューやゲームなど）

またCameraXViewfinderは引数に`MutableCoordinateTransformer`を取り、composeの座標系とCameraXの座標系を変換するために使われています。こちら再掲になりますが[part2](https://medium.com/androiddevelopers/tap-to-focus-mastering-camerax-transformations-in-jetpack-compose-440853280a6e)に使い方が詳しく書かれています。

CameraXViewfinderの中身を見ますと、`rememberUpdateState`や`produceState`、`snapshotFlow`などが使われていて、recompositionを適切にやっているんではないかと思います。

またSurfaceViewのライフサイクルを適切に処理しているようなコードも見受けられます。

最後に、今回はCameraXのCompose対応について触れましたが、いろいろなものにCompose対応が入って開発がさらに便利になっているように感じます。
ありがたいことですね。