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" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index aa2336a..331932b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
Programmatic Based
Viewfinder Based
Why camera?
+ CameraX Example
We use your camera to scan barcodes
diff --git a/kbarcode/src/main/java/uk/co/brightec/kbarcode/BarcodeView.kt b/kbarcode/src/main/java/uk/co/brightec/kbarcode/BarcodeView.kt
index 5f380d9..529a834 100644
--- a/kbarcode/src/main/java/uk/co/brightec/kbarcode/BarcodeView.kt
+++ b/kbarcode/src/main/java/uk/co/brightec/kbarcode/BarcodeView.kt
@@ -197,7 +197,8 @@ public class BarcodeView @JvmOverloads constructor(
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val childRect = calculateRectForChild(
- previewScaleType = _previewScaleType, left = left, top = top, right = right, bottom = bottom
+ previewScaleType = _previewScaleType,
+ left = left, top = top, right = right, bottom = bottom
)
for (i in 0 until childCount) {