Skip to content
This repository has been archived by the owner on Mar 8, 2024. It is now read-only.

Commit

Permalink
task/camerax-example: Added camerax example of scanning a barcode.
Browse files Browse the repository at this point in the history
This will server as a comparison between our lib and camerax.
  • Loading branch information
alistairsykes committed Jun 17, 2021
1 parent 5dd2d1e commit a0e9bd8
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 2 deletions.
5 changes: 5 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
android:name="android.hardware.camera"
android:required="true" />

<!-- For CameraX example only -->
<uses-permission android:name="android.permission.CAMERA" />

<application
android:name=".Application"
android:allowBackup="true"
Expand All @@ -30,6 +33,7 @@
<activity android:name=".XmlJavaActivity" />
<activity android:name=".ProgrammaticActivity" />
<activity android:name=".viewfinder.ViewfinderActivity" />
<activity android:name=".camerax.CameraXActivity" />
<!--suppress AndroidDomInspection - For espresso tests-->
<activity android:name=".testutil.SingleFragmentActivity" />
</application>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/uk/co/brightec/kbarcode/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -27,5 +28,9 @@ class MainActivity : AppCompatActivity() {
val intent = ViewfinderActivity.getStartingIntent(this)
startActivity(intent)
}
button_camerax.setOnClickListener {
val intent = CameraXActivity.getStartingIntent(this)
startActivity(intent)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Barcode>) -> 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())
}
}
Original file line number Diff line number Diff line change
@@ -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<String>,
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)
}
}
24 changes: 24 additions & 0 deletions app/src/main/res/layout/activity_camerax.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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"
android:keepScreenOn="true"
tools:background="@color/black">

<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<TextView
android:id="@+id/text_barcodes"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:background="@color/black"
android:minLines="2"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
12 changes: 11 additions & 1 deletion app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />

<Button
android:id="@+id/button_camerax"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_camerax"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_viewfinder" />
</androidx.constraintlayout.widget.ConstraintLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<string name="title_programmatic">Programmatic Based</string>
<string name="title_viewfinder">Viewfinder Based</string>
<string name="title_camera_rationale">Why camera?</string>
<string name="title_camerax">CameraX Example</string>

<string name="message_camera_rationale">We use your camera to scan barcodes</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit a0e9bd8

Please sign in to comment.