この記事では主に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の記事では、タップしてフォーカスしたときにクラッカーのようなエフェクトを出現させています。
まだ出たばかりなのでドキュメントもこれから増えていくかもしれませんが、すでに公式のMediumの記事もあり、今からでも手を付けやすそうです。
こちらの記事がとてもわかり易いです。この記事の執筆時点では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のアプリでも使いやすくなりましたね。

おまけ:コードの中を見てみる
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.foundation
のAndroidExternalSurface
、後者は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の中身を見ますと、rememberUpdateState
やproduceState
、snapshotFlow
などが使われていて、recompositionを適切にやっているんではないかと思います。
またSurfaceViewのライフサイクルを適切に処理しているようなコードも見受けられます。
最後に、今回はCameraXのCompose対応について触れましたが、いろいろなものにCompose対応が入って開発がさらに便利になっているように感じます。
ありがたいことですね。