From a0e9bd83cb42bab9480cd799c88b26b8bf07816c Mon Sep 17 00:00:00 2001 From: Alistair Sykes Date: Thu, 17 Jun 2021 11:08:16 +0100 Subject: [PATCH] task/camerax-example: Added camerax example of scanning a barcode. This will server as a comparison between our lib and camerax. --- app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 4 + .../co/brightec/kbarcode/app/MainActivity.kt | 5 + .../kbarcode/app/camerax/BarcodeAnalyzer.kt | 70 +++++++ .../kbarcode/app/camerax/CameraXActivity.kt | 181 ++++++++++++++++++ app/src/main/res/layout/activity_camerax.xml | 24 +++ app/src/main/res/layout/activity_main.xml | 12 +- app/src/main/res/values/strings.xml | 1 + .../uk/co/brightec/kbarcode/BarcodeView.kt | 3 +- 9 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/uk/co/brightec/kbarcode/app/camerax/BarcodeAnalyzer.kt create mode 100644 app/src/main/java/uk/co/brightec/kbarcode/app/camerax/CameraXActivity.kt create mode 100644 app/src/main/res/layout/activity_camerax.xml diff --git a/app/build.gradle b/app/build.gradle index 1f9bfff..08aa5d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,11 @@ dependencies { implementation "com.google.android.material:material:1.3.0" implementation platform("com.google.firebase:firebase-bom:28.1.0") implementation "com.google.firebase:firebase-core" + // For CameraX example only + implementation "androidx.camera:camera-camera2:1.0.0" + implementation "androidx.camera:camera-lifecycle:1.0.0" + implementation "androidx.camera:camera-view:1.0.0-alpha25" + implementation "com.google.android.gms:play-services-mlkit-barcode-scanning:16.1.5" //region Local Unit Tests testImplementation "junit:junit:4.13.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 733c84d..54e2ed6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,9 @@ android:name="android.hardware.camera" android:required="true" /> + + + + diff --git a/app/src/main/java/uk/co/brightec/kbarcode/app/MainActivity.kt b/app/src/main/java/uk/co/brightec/kbarcode/app/MainActivity.kt index 9c14852..7069c74 100644 --- a/app/src/main/java/uk/co/brightec/kbarcode/app/MainActivity.kt +++ b/app/src/main/java/uk/co/brightec/kbarcode/app/MainActivity.kt @@ -3,6 +3,7 @@ package uk.co.brightec.kbarcode.app import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_main.* +import uk.co.brightec.kbarcode.app.camerax.CameraXActivity import uk.co.brightec.kbarcode.app.viewfinder.ViewfinderActivity class MainActivity : AppCompatActivity() { @@ -27,5 +28,9 @@ class MainActivity : AppCompatActivity() { val intent = ViewfinderActivity.getStartingIntent(this) startActivity(intent) } + button_camerax.setOnClickListener { + val intent = CameraXActivity.getStartingIntent(this) + startActivity(intent) + } } } diff --git a/app/src/main/java/uk/co/brightec/kbarcode/app/camerax/BarcodeAnalyzer.kt b/app/src/main/java/uk/co/brightec/kbarcode/app/camerax/BarcodeAnalyzer.kt new file mode 100644 index 0000000..5348385 --- /dev/null +++ b/app/src/main/java/uk/co/brightec/kbarcode/app/camerax/BarcodeAnalyzer.kt @@ -0,0 +1,70 @@ +package uk.co.brightec.kbarcode.app.camerax + +import android.content.Context +import android.util.Log +import androidx.annotation.OptIn +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.core.content.ContextCompat +import com.google.android.gms.tasks.Tasks +import com.google.mlkit.vision.barcode.Barcode +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.common.InputImage +import java.util.concurrent.ExecutionException + +internal class BarcodeAnalyzer( + context: Context, + private val barcodeListener: (List) -> Unit, +) : ImageAnalysis.Analyzer { + + private val tag = BarcodeAnalyzer::class.simpleName ?: "BarcodeAnalyzer" + private val mainExecutor = ContextCompat.getMainExecutor(context) + private val scanner: BarcodeScanner = createScanner() + + @OptIn(ExperimentalGetImage::class) + override fun analyze(imageProxy: ImageProxy) { + val rotationDegrees = imageProxy.imageInfo.rotationDegrees + val image = imageProxy.image + if (image != null) { + val inputImage = InputImage.fromMediaImage(image, rotationDegrees) + val task = scanner.process(inputImage) + try { + val result = Tasks.await(task) + if (result.isNotEmpty()) { + mainExecutor.execute { + barcodeListener.invoke(result) + } + } + } catch (e: ExecutionException) { + Log.e(tag, "Scanner process failed", e) + } catch (e: InterruptedException) { + Log.e(tag, "Scanner process interrupted", e) + } + } + imageProxy.close() + } + + private fun createScanner(): BarcodeScanner { + val options = BarcodeScannerOptions.Builder() + val formats = intArrayOf( + Barcode.FORMAT_CODABAR, + Barcode.FORMAT_EAN_13, + Barcode.FORMAT_EAN_8, + Barcode.FORMAT_ITF, + Barcode.FORMAT_UPC_A, + Barcode.FORMAT_UPC_E + ) + if (formats.size > 1) { + @Suppress("SpreadOperator") // Required by Google API + options.setBarcodeFormats( + formats[0], *formats.slice(IntRange(1, formats.size - 1)).toIntArray() + ) + } else if (formats.size == 1) { + options.setBarcodeFormats(formats[0]) + } + return BarcodeScanning.getClient(options.build()) + } +} diff --git a/app/src/main/java/uk/co/brightec/kbarcode/app/camerax/CameraXActivity.kt b/app/src/main/java/uk/co/brightec/kbarcode/app/camerax/CameraXActivity.kt new file mode 100644 index 0000000..8c51ded --- /dev/null +++ b/app/src/main/java/uk/co/brightec/kbarcode/app/camerax/CameraXActivity.kt @@ -0,0 +1,181 @@ +package uk.co.brightec.kbarcode.app.camerax + +import android.Manifest +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.view.MotionEvent +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.camera.core.Camera +import androidx.camera.core.CameraControl +import androidx.camera.core.CameraSelector +import androidx.camera.core.FocusMeteringAction +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import kotlinx.android.synthetic.main.activity_camerax.* +import kotlinx.android.synthetic.main.activity_camerax.text_barcodes +import kotlinx.android.synthetic.main.activity_programmatic.* +import uk.co.brightec.kbarcode.app.R +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +internal class CameraXActivity : + AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { + + private val tag = CameraXActivity::class.simpleName ?: "CameraXActivity" + + private lateinit var cameraExecutor: ExecutorService + private lateinit var camera: Camera + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_camerax) + setTitle(R.string.title_camerax) + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED + ) { + requestCameraPermission() + } else { + startCamera() + } + + cameraExecutor = Executors.newSingleThreadExecutor() + } + + override fun onDestroy() { + cameraExecutor.shutdown() + super.onDestroy() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + when (requestCode) { + REQUEST_PERMISSION_CAMERA -> if ( + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + startCamera() + } + else -> + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + private fun requestCameraPermission() { + if (ActivityCompat.shouldShowRequestPermissionRationale( + this, + Manifest.permission.CAMERA + ) + ) { + AlertDialog.Builder(this) + .setTitle(R.string.title_camera_rationale) + .setMessage(R.string.message_camera_rationale) + .setPositiveButton(R.string.action_ok) { _: DialogInterface, _: Int -> + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.CAMERA), + REQUEST_PERMISSION_CAMERA + ) + } + .show() + } else { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.CAMERA), + REQUEST_PERMISSION_CAMERA + ) + } + } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(this) + cameraProviderFuture.addListener( + { + val previewUseCase = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(preview.surfaceProvider) + } + val imageAnalyzerUseCase = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer( + cameraExecutor, + BarcodeAnalyzer(this) { barcodes -> + val builder = StringBuilder() + for (barcode in barcodes) { + builder.append(barcode.displayValue).append("\n") + } + text_barcodes.text = builder.toString() + } + ) + } + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + cameraProvider.unbindAll() + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + camera = cameraProvider.bindToLifecycle( + this, cameraSelector, previewUseCase, imageAnalyzerUseCase + ) + setupTapToFocus() + }, + ContextCompat.getMainExecutor(this) + ) + } + + private fun setupTapToFocus() { + preview.setOnTouchListener { view, event -> + val actionMasked = event.actionMasked + if (actionMasked == MotionEvent.ACTION_UP) { + view.performClick() + return@setOnTouchListener false + } + if (actionMasked != MotionEvent.ACTION_DOWN) { + return@setOnTouchListener false + } + + val cameraControl = camera.cameraControl + val factory = preview.meteringPointFactory + val point = factory.createPoint(event.x, event.y) + val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF) + .addPoint(point, FocusMeteringAction.FLAG_AE) + .addPoint(point, FocusMeteringAction.FLAG_AWB) + .build() + val future = cameraControl.startFocusAndMetering(action) + future.addListener( + { + try { + val result = future.get() + Log.d(tag, "Focus Success: ${result.isFocusSuccessful}") + } catch (e: CameraControl.OperationCanceledException) { + Log.d(tag, "Focus cancelled") + } catch (e: ExecutionException) { + Log.e(tag, "Focus failed", e) + } catch (e: InterruptedException) { + Log.e(tag, "Focus interrupted", e) + } + }, + cameraExecutor + ) + true + } + } + + companion object { + + private const val REQUEST_PERMISSION_CAMERA = 1 + + fun getStartingIntent(context: Context) = Intent(context, CameraXActivity::class.java) + } +} diff --git a/app/src/main/res/layout/activity_camerax.xml b/app/src/main/res/layout/activity_camerax.xml new file mode 100644 index 0000000..9e06619 --- /dev/null +++ b/app/src/main/res/layout/activity_camerax.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5ae50b9..bc6f4cd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -41,8 +41,18 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title_viewfinder" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/button_camerax" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button_programmatic" /> + +