📸

cameraxをcomposeで扱いやすくするライブラリを作ってみた

2024/02/29に公開1

自作ライブラリ camerax-compose をリリースしてみたので宣伝 兼ねてアウトプットします。

https://github.com/TBSten/camerax-compose

camerax-composeとは?

jetpack composeで極力簡単にカメラを扱えるようにすることを目指しているライブラリです。

CameraXは、Androidでカメラアプリの開発を容易にするために作られたライブラリと言われていますが、実際のところはJetpack Composeに対応していなかったり、javaで書かれているためにkotlinだとちょっと書きにくいといった扱いにくさが難点です。

CameraXはComposeアプリと組み合わせるためには AndroidView Composableを利用してラップしたりする必要がありますが、下手にラップしてしまうと「これAndroid Viewで書いた方が綺麗じゃね?」ってなりがちです。

そこでjetpack compose(kotlin)で手軽にカメラを扱えるようにうまーくcameraxをラップしてみたライブラリが camerax-compose になります。

コンセプト

  • Jetpack Composeで
  • 少ないコード量で簡単に
  • なるべく本家(CameraX)に沿って

カメラ機能を実装できるようにラップしています。

また 既存のCameraXリソースとの統合のしやすさも強みとしたかったので、既存のUseCaseをそのまま使えるように実装しています。

以下は一番ミニマムな例です。

val context = LocalContext.current

CameraPreview(
    onBind = {
        val executor = ContextCompat.getMainExecutor(context)
        // ユースケース
        val preview = previewUseCase()
        // ユースケースをbindします
        cameraProvider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview,
        )
    },
)
  • CameraPreview Composableを呼び出すことでCameraPreviewが表示されます。
  • 引数のonBindが初期化時のコールバックになります。主にCameraXのUseCaseをバインドするために使います。

書くことが多くて大変だったカメラの実装がたったこれだけで済むの感動しませんか???!!!!

導入手順

kotlinだったら以下の通りです。groovyの場合はREADMEを参照してください。

また CameraX に依存しているのでそれも忘れずに導入しておく必要があります。

CameraXの公式ドキュメントなどを参考に導入しておいてください。

// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = URI("https://jitpack.io") } // 追加
    }
}

// app/build.gradle.kts
dependencies {
    // TODO cameraxの依存関係を追加すること
    // 詳しくはこちら: https://developer.android.com/jetpack/androidx/releases/camera#dependencies
    implementation("com.github.tbsten:camerax-compose:<current-version>")
}

実装していく上でのポイント/勉強になったこと

コールバックをcoroutineに書き換える

CameraXのImageCapture UseCaseは以下のように takePicture() を呼び出してその最後の引数にコールバックを渡す形式になっています。

imageCapture.takePicture(
  executor,
  object : ImageCapture.OnImageCapturedCallback() {
    override fun onCaptureSuccess(image: ImageProxy) {
      super.onCaptureSuccess(image)
      continuation.resume(image)
    }

    override fun onError(exception: ImageCaptureException) {
      super.onError(exception)
      continuation.cancel(exception)
    }
  },
)

これだと非常にわかりにくいので同期的にかけるように suspendCancellableCoroutine を利用して違うシグネチャの拡張関数を生やしてみました。

suspend fun ImageCapture.takePicture(executor: Executor): ImageProxy =
    suspendCancellableCoroutine { continuation ->
        takePicture(
            executor,
            object : ImageCapture.OnImageCapturedCallback() {
                override fun onCaptureSuccess(image: ImageProxy) {
                    super.onCaptureSuccess(image)
                    continuation.resume(image)
                }

                override fun onError(exception: ImageCaptureException) {
                    super.onError(exception)
                    continuation.cancel(exception)
                }
            },
        )
    }

これで呼び出し側からは以下のようにすっきり呼び出すことができます。

val image = imageCapture.takePicture(executor)

正直個人的にはここが一番勉強になったところだったりします。coroutineナンモワカラン

UseCaseのラップ

公式でも提供されている4つのUseCaseをよりスマートに、Kotlinらしく書けるUseCaseのラッパー関数をいくつか用意しました。(もちろん本家の方がいい!って人は本家の方から引っ張ってきたUseCaseをbindすることもできます)

- val preview = Preview().Builder().build()
+ val preview = previewUseCase()

元々はPreview UseCaseの setSurfaceProvider 呼び忘れがち問題を解消したかったことがきっかけで、「ライブラリ側からPreview UseCaseのsetSurfaceProvider()を自動で呼んでおく方法」を考えた結果、onBindのラムダ内から呼び出した previewUseCase() 内で setSurfaceProvider() を呼び出すようにしました。

CameraPreview(
    onBind = {
        val executor = ContextCompat.getMainExecutor(context)
        // ユースケース
        val preview = previewUseCase()    // 👈 この中でpreview.setSurfaceProvider(previewView.surfaceProvider)してくれるので呼び忘れがない!
        // ユースケースをbindします
        cameraProvider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview,
        )

またpreviewだけ関数で生成するのもなんか違和感だったので、ImageCapture, ImageAnalysis, VideoCaptureにもついてでにラッパーを用意して短く書けるようにしておきました。個人的には Build が何度も出てきてうーん🤨って感じていたので改善かなと思います。

  CameraPreview(
    onBind = {
      val executor = ContextCompat.getMainExecutor(context)
      // UseCases
-     val preview = Preview.Builder().build()
-     val analysis = ImageAnalysis.Builder().build()
-     imageCapture = ImageCapture.Builder().build()
-     videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
+     val preview = previewUseCase()
+     val analysis = imageAnalysisUseCase(executor) {}
+     imageCapture = imageCaptureUseCase()
+     videoCapture = videoCaptureUseCase()
      // bind
      cameraProvider.bindToLifecycle(
        lifecycleOwner,
        CameraSelector.DEFAULT_BACK_CAMERA,
        preview,
        analysis,
        imageCapture,
        videoCapture,
      )
    },
  )

とはいってもこのあたりは個人的な思想と言えばそうなので 変えが効くように しています。
具体的にはラッパー関数を用意するも、その戻り値は元々のUseCaseの型と同じにしています。

// 本家のUseCaseの組み立て方
val preview = Preview().Builder().build()
// UseCaseのラッパー関数での組み立て方
val preview = previewUseCase()

// ラッパーを使うやり方でも使わないやり方でもどちらでも同じようなことができる

ライブラリの公開

公開のリポジトリには、Gitを連携させてライブラリを公開できるjitpackを利用しました。

ライブラリの公開が初めてだったのでわからないことだらけでした。とはいってもjitpackやandroid公式のドキュメントが充実していたので半日もかからずに公開できました😊

https://docs.jitpack.io/android/

https://developer.android.com/build/publish-library

感想:やってないことだらけ

現状ドキュメントが頼りないREADME一枚だったり、LintやCI/CDの導入もしていないなどなどやらないといけないことが山盛りなので少しずつ対応していけたらと思います。