1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CameraXとそのCopmose対応について

Posted at

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

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

CameraX の概要 Android Developers

CameraXとCompose対応について

2024年末のAndroid Developer Blogにて、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の記事では、タップしてフォーカスしたときにクラッカーのようなエフェクトを出現させています。

comeraX

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

Create a spotlight effect with CameraX and Jetpack Compose | by Jolanda Verhoef | Android Developers | Jan, 2025 | Medium

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

part2
part3

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

従来Viewのサンプルはandroid/camera-samplesにあります。

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

前述のCameraX の概要 Android Developersに詳細は書いてありますが、簡単にキーワードに触れていきます。

ユースケース

ユースケースとは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

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

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

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

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で公開します。

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にしてみましたが、共有ストレージへの保存でもよいです)

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を使った開発と同じように配置していけば良いので楽です。

@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のアプリでも使いやすくなりましたね。

CameraX

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

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

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

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

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

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

/**
 * 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に使い方が詳しく書かれています。

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

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?