📱

Android:CameraXをつかったバーコードスキャンをON・OFFする方法

2022/10/13に公開

はじめに

バーコードスキャンや画像解析、画像・動画キャプチャをする際、JetpackライブラリのCameraXが便利です。
CameraXをつかったバーコードスキャンをON/OFFする方法について説明します。
例えば、バーコードスキャンした後で一度処理を止めたい、といったときに使うことができます。
サンプルコードはバーコードスキャンを行い、設置されているボタン「Stop」を押すとバーコードスキャンを停止、ボタン「Start」を押すとバーコードスキャンを開始します。

やり方

サンプルコードの全体

サンプルコードは下記のようになります。
さらにパーミッションの設定も必要になります。

MainActivity.kt
MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var cameraExecutors: ExecutorService
    private lateinit var scanner: BarcodeScanner
    private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    private lateinit var cameraProvider: ProcessCameraProvider
    private lateinit var previewUseCase: Preview
    private lateinit var analysisUseCase: ImageAnalysis
    private lateinit var cameraSelector: CameraSelector

    private val PERMISSION_WRITE_EX_STR = 1

    private val PERMISSIONS = arrayOf(
        Manifest.permission.CAMERA,
    )

    @RequiresApi(Build.VERSION_CODES.R)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // パーミッションをリクエスト
        permissionRequest()

        // スレッドを作成
        cameraExecutors = Executors.newSingleThreadExecutor()

        bindCameraUseCases()

        val stopButton = findViewById<Button>(R.id.stopButton)
        val startButton = findViewById<Button>(R.id.startButton)

        stopButton.setOnClickListener {
            stopPreview()
        }
        startButton.setOnClickListener {
            bindCameraUseCases()
            startPreview()
        }

    }

    fun bindCameraUseCases() {
        val previewView = findViewById<PreviewView>(R.id.previewView)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            cameraProvider = cameraProviderFuture.get()

            // preview部分にカメラ画像を表示
            previewUseCase = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }
            /*
            スキャンできるバーコードの型を設定しておくことが可能
            未指定の場合は自動判定が可能
            今回はコメントアウト
             */
//            BarcodeScannerOptions.Builder().setBarcodeFormats(
//                Barcode.FORMAT_QR_CODE,
//                Barcode.FORMAT_CODABAR,
//                Barcode.FORMAT_CODE_128,
//                Barcode.FORMAT_CODE_39,
//                Barcode.FORMAT_CODE_93,
//                Barcode.FORMAT_EAN_8,
//                Barcode.FORMAT_EAN_13,
//            ).build()

            // CameraSelector.DEFAULT_BACK_CAMERA  背面カメラ
            // CameraSelector.DEFAULT_FRONT_CAMERA 前面カメラ
            cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            scanner = BarcodeScanning.getClient()
            analysisUseCase = ImageAnalysis.Builder().build()
            analysisUseCase.setAnalyzer(cameraExecutors)
            { imageProxy -> processImageProxy(scanner, imageProxy) }
            startPreview()
        }, ContextCompat.getMainExecutor(this))
    }

    @SuppressLint("UnsafeOptInUsageError")
    private fun processImageProxy(barcodeScanner: BarcodeScanner, imageProxy: ImageProxy, ) {
        imageProxy.image?.let { image ->
            val inputImage =
                InputImage.fromMediaImage(
                    image,
                    imageProxy.imageInfo.rotationDegrees
                )

            barcodeScanner.process(inputImage)
                .addOnSuccessListener { barcodeList ->
                    val barcode = barcodeList.getOrNull(0)
                    barcode?.rawValue?.let { value ->
                        if(value.isNotEmpty()){
                            val barcodeView = findViewById<TextView>(R.id.barcodeView)
                            barcodeView.setText(value)
                        }
                    }
                }
                .addOnFailureListener {
                    Log.e(TAG, it.message.orEmpty())
                }.addOnCompleteListener {
                    imageProxy.image?.close()
                    imageProxy.close()
                }
        }
    }

    fun startPreview(){
        try {
            cameraProvider.bindToLifecycle(
                this,
                cameraSelector,
                previewUseCase,
                analysisUseCase)
        } catch (illegalStateException: IllegalStateException) {
            Log.e(TAG, illegalStateException.message.orEmpty())
        } catch (illegalArgumentException: IllegalArgumentException) {
            Log.e(TAG, illegalArgumentException.message.orEmpty())
        }
    }

    fun stopPreview(){
        // previewViewへの投影を停止
        cameraProvider.unbind(previewUseCase)
        // バーコードスキャンを停止
        scanner.close()
    }

    // パーミッションのリクエスト
    private fun permissionRequest(){
        if (Build.VERSION.SDK_INT >= 23) {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
            ) {
                ActivityCompat.requestPermissions(this, PERMISSIONS, PERMISSION_WRITE_EX_STR)
            }else{
                Log.d("Permission is", "OK")
            }
        }
    }

}
activity_main.xml
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.196">
    </androidx.camera.view.PreviewView>

    <Button
        android:id="@+id/stopButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="300dp"
        android:layout_marginBottom="25dp"
        android:text="Stop"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/startButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="300dp"
        android:layout_marginBottom="25dp"
        android:text="Start"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/barcodeView"
        android:layout_width="400dp"
        android:layout_height="40dp"
        android:layout_marginBottom="25dp"
        android:text=""
        android:textColor="@color/white"
        android:textSize="35dp"
        android:gravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/previewView"
        app:layout_constraintVertical_bias="0.893" />

</androidx.constraintlayout.widget.ConstraintLayout>

ライブラリの追加

CameraXを使うために、app/build.gradleにモジュールを追加します。

build.gradle
dependencies {
    // CameraX
    def cameraxVersion = "1.2.0-alpha04"
    implementation "androidx.camera:camera-core:${cameraxVersion}"
    implementation "androidx.camera:camera-camera2:${cameraxVersion}"
    implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"
    implementation "androidx.camera:camera-video:${cameraxVersion}"
    implementation "androidx.camera:camera-view:${cameraxVersion}"
    implementation "androidx.camera:camera-extensions:${cameraxVersion}"

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

カメラのパーミッションを通す

カメラを使用するのでパーミッションの設定が必要になります。
AndroidManifest.xmlに下記を追加します。

AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />

そして、MainActivity.ktでは下記の関数を呼び出すことでパーミッションをリクエストしています。

MainActivity.kt
private val PERMISSION_WRITE_EX_STR = 1

private val PERMISSIONS = arrayOf(
    Manifest.permission.CAMERA,
)

// パーミッションのリクエスト
private fun permissionRequest(){
    if (Build.VERSION.SDK_INT >= 23) {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(this, PERMISSIONS, PERMISSION_WRITE_EX_STR)
        }else{
            Log.d("Permission is", "OK")
        }
    }
}

カメラの映像を表示

先程、activity_main.xmlのコードを示しました。
CameraXをつかって、カメラの映像をアプリ上でストリーミングするには PreviewViewをレイアウトに追加します。

activity_main.xml
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.196">
</androidx.camera.view.PreviewView>

そして、このPreviewViewに映像をひもづけます。
その処理はMainActivity.ktのbindCameraUseCases()関数内で行われています。

MainActivity.kt
    fun bindCameraUseCases() {
        val previewView = findViewById<PreviewView>(R.id.previewView)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            cameraProvider = cameraProviderFuture.get()

            // preview部分にカメラ画像を表示
            previewUseCase = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }
            /*
            スキャンできるバーコードの型を設定しておくことが可能
            未指定の場合は自動判定が可能
            今回はコメントアウト
             */
//            BarcodeScannerOptions.Builder().setBarcodeFormats(
//                Barcode.FORMAT_QR_CODE,
//                Barcode.FORMAT_CODABAR,
//                Barcode.FORMAT_CODE_128,
//                Barcode.FORMAT_CODE_39,
//                Barcode.FORMAT_CODE_93,
//                Barcode.FORMAT_EAN_8,
//                Barcode.FORMAT_EAN_13,
//            ).build()

            // CameraSelector.DEFAULT_BACK_CAMERA  背面カメラ
            // CameraSelector.DEFAULT_FRONT_CAMERA 前面カメラ
            cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            scanner = BarcodeScanning.getClient()
            analysisUseCase = ImageAnalysis.Builder().build()
            analysisUseCase.setAnalyzer(cameraExecutors)
            { imageProxy -> processImageProxy(scanner, imageProxy) }
            startPreview()
        }, ContextCompat.getMainExecutor(this))
    }

上記の処理はAndroid Developerの「プレビューを実装する」で詳細説明されています。
かいつまんで説明すると、下記のような手順を踏んでいます。

  1. CameraProviderをリクエスト
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  1. CameraProviderが利用可能かどうかを確認
MainActivity.kt
    fun bindCameraUseCases() {
        val previewView = findViewById<PreviewView>(R.id.previewView)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            cameraProvider = cameraProviderFuture.get()
	/*
	省略
	*/
            startPreview()
        }, ContextCompat.getMainExecutor(this))
  1. カメラを選択してライフサイクルとユースケースをバインドします。こちらはstartPreview()関数内で処理しています。
MainActivity.xml
fun startPreview(){
    try {
        cameraProvider.bindToLifecycle(
            this,
            cameraSelector,
            previewUseCase,
            analysisUseCase)
    } catch (illegalStateException: IllegalStateException) {
        Log.e(TAG, illegalStateException.message.orEmpty())
    } catch (illegalArgumentException: IllegalArgumentException) {
        Log.e(TAG, illegalArgumentException.message.orEmpty())
    }
}

バーコードスキャン

バーコードスキャンする場合は、Previewと同じようにAnalyzerをバインドします。
下記で記述したanalysisUseCaseをstartPreview()関数のbindToLifecycle内に渡します。

MainActivity.kt
analysisUseCase = ImageAnalysis.Builder().build()
analysisUseCase.setAnalyzer(cameraExecutors)
{ imageProxy -> processImageProxy(scanner, imageProxy) }

バーコードスキャンのON/OFF

バーコードスキャンのONにするときはstartPreview()を呼び出します。
OFFにするときはstopPreview()を呼び出します。
stopPreview()では、previewViewへのストリーミングを停止(unbind)し、BarcodeScanning.getClient()でインスタンス作成したscannerをcloseします。
そうするとバーコードスキャンの処理が停止します。

MainActivity.kt
    fun startPreview(){
        try {
            cameraProvider.bindToLifecycle(
                this,
                cameraSelector,
                previewUseCase,
                analysisUseCase)
        } catch (illegalStateException: IllegalStateException) {
            Log.e(TAG, illegalStateException.message.orEmpty())
        } catch (illegalArgumentException: IllegalArgumentException) {
            Log.e(TAG, illegalArgumentException.message.orEmpty())
        }
    }

    fun stopPreview(){
        // previewViewへのストリーミングを停止
        cameraProvider.unbind(previewUseCase)
        // バーコードスキャンを停止
        scanner.close()
    }

おわりに

上記のやり方でCameraXをつかったバーコードスキャンをON/OFFすることができました。

Discussion