見出し画像

【Android/Kotlin】CameraXでQRコードリーダーを作ろう

こんにちはAndroidアプリエンジニアをしているまっこりです!
今回のnoteは、CameraXとMLKitを使ってQRコードリーダーアプリを作るチュートリアルとなってます。UIはJetpackComposeで実装していきます。

完成するとこんな感じになります。

CameraXはカメラアプリの開発を容易にすることを目的としたライブラリで、developerサイトでも新しいアプリを開発する場合はCameraXの使用を推奨しています。

使用するライブラリ
- CameraX
- MLKit(barcode-scanning)
- Jetpack Compose

開発環境
- Android Studio Chipmunk | 2021.2.1 Patch 1
- CameraX: 1.2.0-alpha01
- MLKit: 17.0.2
- jetpack compose: 1.1.0-beta01

前提条件
- エミュレータや実機でのデバック方法がわかる
- プロジェクトの作成方法などAndroidStudioの基本的な使い方がわかる
* エミュレータでもQRコードスキャンの動作確認はできるので実機がない方でも大丈夫です(エミュレータでカスタム画像を設定する方法)
推奨条件
- coroutineへの多少の理解

完成版のソースコードはこちらにアップロードしてあります。必要な場合は参照ください

1. 新規プロジェクトの作成

まずはプロジェクトを新規作成します。テンプレートは「Empty Compose Activity」を選択してください。

テンプレートを選択したら、次はアプリ名を入力します。アプリ名は好きなものを入力しましょう、迷った場合は「QRCodeScanner」とつけておきましょう。アプリ名が入力できたらその他の項目は変更せずに「Finish」を押してください。選択したテンプレートに対応したプロジェクトが作成されます。

プロジェクトが作成されたら、一度エミュレーターか実機を起動してみましょう。

2. ライブラリ依存関係の追加

必要なライブラリの依存関係を追加していきます。
Moduleレベルのbuild.gradleファイルを開いて、ファイルの一番下のdependencies{…}となっている箇所に以下の記述を追加してください。

// CameraX
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"

// MLKit barcode-scanning
implementation 'com.google.mlkit:barcode-scanning:17.0.2'

これで、今回使用するCameraXとMLKitの依存関係の追加ができました。エディタ右上に表示される「Sync Now」というボタンを押してライブラリをインストールしましょう。

この時点で、build.gradleのdependenciesブロックが以下のようになっていればOKです。

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"

    // CameraX
    def camerax_version = "1.2.0-alpha01"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-video:${camerax_version}"
    implementation "androidx.camera:camera-view:${camerax_version}"
    implementation "androidx.camera:camera-extensions:${camerax_version}"

    // MLKit barcode-scanning
    implementation 'com.google.mlkit:barcode-scanning:17.0.2'
}

Jetpack Composeのテンプレートを選択したので、既にcompose系のdependenciesは追加されています。

3. 不要なコードの削除とUIベースの作成

実際にQRコード読み取り機能を作っていく前に、テンプレートに含まれる不要なコードの削除と機能を追加していくためのベースの作成を行なっていきます。

不要なコードの削除

MainActivityを開き、ファイル下部にあるGreetingとDefaultPreviewというコンポーネントを削除してください。また、Greetingを呼び出している箇所も削除してください。

(削除する箇所)
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    QRCodeScannerTheme {
        Greeting("Android")
    }
}

削除後のMainActivityは以下のようになるかと思います。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            QRCodeScannerTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    // Greetingの呼び出しを削除
                }
            }
        }
    }
}

UIベースの追加

不要なコードを追加したら、この後でUIのコンポーネントを積んでいくためのベースとなるJetpackComposeのコンポーネントを追加します。

MainAcityの下に以下のような空のコンポーネント定義を追加してください。

@Composable
fun MainContent() {

}

これを先ほどのGreetingの呼び出しを削除した箇所に追加します。

Surface(
    modifier = Modifier.fillMaxSize(),
    color = MaterialTheme.colors.background
) {
    MainContent() // ここ
}

このMainContentにカメラプレビューを追加していく形で実装していきます。

4. カメラプレビューUIの作成

QRコードの解析機能を作る前に、カメラで取得中の画像を画面上に表示する機能を作っていきます。

まず、MainActivityと同じディレクトリにCameraPreview.ktというファイルを作成します。(classではなくKotlin fileを作成)

作成したら、以下のように記述してください。

@Composable
fun CameraPreview(modifier: Modifier) {
    
}

このコンポーネントの中にAndroidViewを使ってカメラプレービュー表示を作っていきます。CameraXライブラリによって提供されているカメラプレビュー用のPreviewViewはViewクラスを継承しているため、JetpackComposeの中でPreviewViewを表示するためAndroidViewを使用します。

CameraXではUseCaseというものを作成し、このUseCaseをカメラのライフサイクルにバインドするとことでカメラ映像を使用した各種機能を開発できます。UseCaseが機能一つ一つに対応しており、今回の場合はQRコードを解析するUseCaseとカメラプレビュー作成用のUseCaseを作成し、カメラのライフサイクルにバインドします。

Contextクラスのエクステンションを作成します。UseCaseをカメラにバインドするためには、ProcessCameraProviderというクラスのインスタンスを取得する必要があるのですが、このインスタンスを取得するためのエクステンションの作成をUseCase作成の前に行なっていきます。

MainActivityと同じディレクトリに、Context.ktというファイルを作成して、以下のようにコードを書いてください。結構難しいコードだと思うので、完全に理解はしなくて大丈夫です。ざっくり説明すると、非同期でProcessCameraProviderのインスタンスを取得しておき、インスタンスの取得が終わったらメインスレッド(or coroutineScope)に非同期処理の終了を通知するといったものです。

import android.content.Context
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation ->
    ProcessCameraProvider.getInstance(this).also { future ->
        future.addListener({
            continuation.resume(future.get())
        }, executor)
    }
}

val Context.executor: Executor
    get() = ContextCompat.getMainExecutor(this)

エクステンションの方が追加できたので、次は、カメラプレビュー用のUIをAndroidViewとカメラプレビューUseCaseの作成をします。CameraPreviewコンポーネントを以下のように変更してください。(詳細は下で説明します。)

@Composable
fun CameraPreview(modifier: Modifier) {
    val coroutineScope = rememberCoroutineScope()
    val lifecycleOwner = LocalLifecycleOwner.current

    AndroidView(
        modifier = modifier,
        factory = { context ->
            val previewView = PreviewView(context).apply {
                this.scaleType = scaleType
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
            }
            // CameraX Preview UseCase
            val previewUseCase = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            coroutineScope.launch {
                val cameraProvider = context.getCameraProvider()
                try {
                    // use caseをライフサイクルにバインドする前にアンバインドを行う必要がある
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, previewUseCase
                    )
                } catch (ex: Exception) {
                    Log.e("CameraPreview", "Use case binding failed", ex)
                }
            }
            previewView
        }
    )
}

上のコードを一手順づつ解説します。

val coroutineScope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current

まず、ProcessCameraProviderインスタンスを取得するときに非同期処理を実行するため、coroutineスコープを作成します。また、UseCaseのライフサイクルスコープを指定するときに使用するLifecycleOwnerを取得します。

val previewView = PreviewView(context).apply {
    this.scaleType = scaleType
    layoutParams = ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT
    )
}

次にfactory内で、AndroidViewで表示するViewのインスタンスを作成します。上のコードではCameraXのPreviewViewのインスタンスを作成して、サイズを指定しています。ちなみに、AndroidViewのfactoryはViewの初回レンダリング時に実行されます。

// CameraX Preview UseCase
val previewUseCase = Preview.Builder()
    .build()
    .also {
        it.setSurfaceProvider(previewView.surfaceProvider)
    }

カメラプレービューのUseCaseを作成。

coroutineScope.launch {
    val cameraProvider = context.getCameraProvider()
    try {
        // use caseをライフサイクルにバインドする前にアンバインドを行う必要がある
        cameraProvider.unbindAll()
        cameraProvider.bindToLifecycle(
            lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, previewUseCase
        )
    } catch (ex: Exception) {
        Log.e("CameraPreview", "Use case binding failed", ex)
    }
}

coroutineスコープ内で、先ほどContextに追加したエクステンションを使い、ProcessCameraProviderインスタンスを取得し、カメラのライフサイクルにカメラプレビューのUseCaseをバインドしています。

ここまでで、カメラプレビュー表示のKotlinの実装は完了です。ただし、この状態でアプリを実行してもカメラプレビューは表示されません。カメラをアプリで使用するにはCameraのパーミッションが必要なので、次は、カメラパーミッションを確認&リクエストする処理を追加していきます。

5. カメラパーミッションの確認 & リクエスト

ここから先は

10,246字 / 3画像
この記事のみ ¥ 250

この記事が気に入ったらチップで応援してみませんか?