Android Barcode Scanner with CameraX

Camera control and barcode reading are two essential parts of a barcode scanner.

Dynamsoft Barcode Reader is an advanced barcode reader with an easy-to-use Android library. The implementation of the barcode reading function is simple and straightforward.

Making a good camera app, however, is not easy. In Android, there are three sets of Camera APIs to use: Camera, Camera2 and CameraX.

Camera can take photos and record videos. But it has limited camera controls. Camera2 provides in-depth controls for complex use cases but requires you to manage device-specific configurations.1 Its usage is complex (The two are compared in a previous post).

CameraX is a newer one. Here is the description from Google2:

CameraX is a Jetpack support library, built to help you make camera app development easier. It provides a consistent and easy-to-use API surface that works across most Android devices, with backward-compatibility to Android 5.0 (API level 21).

While CameraX leverages the capabilities of camera2, it uses a simpler approach that is lifecycle-aware and is based on use cases. It also resolves device compatibility issues for you so that you don’t have to include device-specific code in your code base. These features reduce the amount of code you need to write when adding camera capabilities to your app.

CameraX has three basic use cases: preview, image analysis and image capture.

Image analysis is designed to facilitate using technologies like ML Kit to analyse buffered images, which is also useful for barcode reading. We don’t have to worry about concurrency as CameraX takes care of this for us.

In this article, we will talk about how to build a barcode scanner with CameraX as shown below.

screenshot

Getting started with Dynamsoft Barcode Reader

Create a New Project

  1. Open Android Studio, create a new project. Choose Empty Activity. Use Java as the language and set the minimum sdk to 21 since CameraX requires at least Android 5.0.
  2. Add Dynamsoft Barcode Reader by following this guide.
  3. Add CameraX by following this guide.

Layout Design

Here is the content of activity_main.xml which defines the layout of the MainActivity. A Button is used to open the camera activity.

<?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">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/container"
            android:textSize="20sp"
            android:text="This is a CameraX test app!"
            android:gravity="center">
        </TextView>
        <Button
            android:id="@+id/enableCamera"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="Open Camera" />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Create a new activity named CameraActivity to show camera preview and barcode reading results. The following is its layout.

<?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=".CameraActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <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">

        </androidx.camera.view.PreviewView>
    </androidx.constraintlayout.widget.ConstraintLayout>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/resultContainer"
        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">

        <TextView
            android:id="@+id/resultView"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:layout_gravity="bottom"
            android:background="#80000000"
            android:gravity="center_horizontal|top"
            android:textColor="#9999ff"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Use CameraX to Preview and Analyse Images

  1. Set event listener for the open camera button.

     protected void onCreate(Bundle savedInstanceState) {
         Button enableCamera = findViewById(R.id.enableCamera);
         enableCamera.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 if (hasCameraPermission()) {
                     startScan();
                 } else {
                     requestPermission();
                 }
             }
         });
     }
     private void startScan(){
         Intent intent = new Intent(this, CameraActivity.class);
         startActivity(intent);
     }
    

    Camera permission is required.

    It will ask the user to grant permission when the camera is needed. Here is the relevant code:

     //properties
     private static final String[] CAMERA_PERMISSION = new String[]{Manifest.permission.CAMERA};
     private static final int CAMERA_REQUEST_CODE = 10;
        
     //methods
     private boolean hasCameraPermission() {
         return ContextCompat.checkSelfPermission(
                 this,
                 Manifest.permission.CAMERA
         ) == PackageManager.PERMISSION_GRANTED;
     }
    
     private void requestPermission() {
         ActivityCompat.requestPermissions(
                 this,
                 CAMERA_PERMISSION,
                 CAMERA_REQUEST_CODE
         );
     }
        
     @Override
     public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                            int[] grantResults) {
         switch (requestCode) {
             case CAMERA_REQUEST_CODE:
                 if (grantResults.length > 0 &&
                         grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                     startScan();
                 }else{
                     Toast.makeText(this, "Please grant camera permission" , Toast.LENGTH_SHORT).show();
                 }
         }
     }
    

    The following has to be added to AndroidManifest.xml:

     <uses-permission android:name="android.permission.CAMERA" />
    
  2. Init and enable preview and image analysis use cases

    Dynamsoft Barcode Reader is used to read barcodes from the image buffer. Here, a public license is used. You can apply for your own license here.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera);
        previewView = findViewById(R.id.previewView);
        resultView = findViewById(R.id.resultView);
        initDBR();
        exec = Executors.newSingleThreadExecutor();
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(new Runnable() {
            @Override
            public void run() {
                try {
                    ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
                    bindPreviewAndImageAnalysis(cameraProvider);
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, ContextCompat.getMainExecutor(this));
    }
        
    private void initDBR(){
        BarcodeReader.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", new DBRLicenseVerificationListener() {
            @Override
            public void DBRLicenseVerificationCallback(boolean isSuccessful, Exception e) {
                if (!isSuccessful) {
                    e.printStackTrace();
                }
            }
        });
        try {
            dbr = new BarcodeReader();
        } catch (BarcodeReaderException e) {
            e.printStackTrace();
        }
    }
        
    @SuppressLint("UnsafeExperimentalUsageError")
    private void bindPreviewAndImageAnalysis(@NonNull ProcessCameraProvider cameraProvider) {
        
        int orientation = getApplicationContext().getResources().getConfiguration().orientation;
        Size resolution;
        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
            resolution = new Size(720, 1280);
        }else{
            resolution = new Size(1280, 720);
        }
    
        Preview.Builder previewBuilder = new Preview.Builder();
        previewBuilder.setTargetResolution(resolution);
        Preview preview = previewBuilder.build();
    
        ImageAnalysis.Builder imageAnalysisBuilder = new ImageAnalysis.Builder();
    
        imageAnalysisBuilder.setTargetResolution(resolution)
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST);
    
        ImageAnalysis imageAnalysis = imageAnalysisBuilder.build();
    
        imageAnalysis.setAnalyzer(exec, new ImageAnalysis.Analyzer() {
            @RequiresApi(api = Build.VERSION_CODES.O)
            @Override
            public void analyze(@NonNull ImageProxy image) {
                int rotationDegrees = image.getImageInfo().getRotationDegrees();
                TextResult[] results = null;
                ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                int nRowStride = image.getPlanes()[0].getRowStride();
                int nPixelStride = image.getPlanes()[0].getPixelStride();
                int length = buffer.remaining();
                byte[] bytes = new byte[length];
                buffer.get(bytes);
                ImageData imageData = new ImageData(bytes, image.getWidth(), image.getHeight(), nRowStride * nPixelStride);
                try {
                    results = dbr.decodeBuffer(imageData.mBytes, imageData.mWidth, imageData.mHeight, imageData.mStride, EnumImagePixelFormat.IPF_NV21, "");
                } catch (BarcodeReaderException e) {
                    e.printStackTrace();
                }
                StringBuilder sb = new StringBuilder();
                sb.append("Found ").append(results.length).append(" barcode(s):\n");
                for (int i = 0; i < results.length; i++) {
                    sb.append(results[i].barcodeText);
                    sb.append("\n");
                }
                Log.d("DBR", sb.toString());
                runOnUiThread(()->{resultView.setText(sb.toString());});
                image.close();
            }
        });
    
        CameraSelector cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
        preview.setSurfaceProvider(previewView.getSurfaceProvider());
    
        UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
                .addUseCase(preview)
                .addUseCase(imageAnalysis)
                .build();
        camera = cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, useCaseGroup);
    }
        
    private class ImageData {
        private int mWidth, mHeight, mStride;
        byte[] mBytes;
    
        ImageData(byte[] bytes, int nWidth, int nHeight, int nStride) {
            mBytes = bytes;
            mWidth = nWidth;
            mHeight = nHeight;
            mStride = nStride;
        }
    }
    

    Here are some things to notice:

    1. Preview and ImageAnalysis should share the same target resolution.
    2. The resolution is set using setTargetResolution. The Size should match the screen orientation. The resolution can also be set using setTargetAspectRatio.
    3. The camera sensor’s natural orientation is landscape. It is rotated based on device rotation for camera preview while the ImageProxy gives the raw image. You can set setOutputImageRotationEnabled to rotate the image to match the camera preview.
    4. The default image format is YUV. You may need to convert it to a RGB bitmap if you need to perform other operations. You can find a converter in Google’s camera samples repo.

More Camera Controls

The CameraControl API offers some basic camera control capabilities.

Torch control:

camera.getCameraControl().enableTorch(true);

Zoom in:

camera.getCameraControl().setLinearZoom((float) 80/100);

Focus at a point:

CameraControl cameraControl=camera.getCameraControl();
MeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(width, height);
MeteringPoint point = factory.createPoint(x, y);
FocusMeteringAction.Builder builder = new FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF);
// auto calling cancelFocusAndMetering in 5 seconds
builder.setAutoCancelDuration(5, TimeUnit.SECONDS);
FocusMeteringAction action =builder.build();
cameraControl.startFocusAndMetering(action);

We can also use Camera2Interop.Extender to have access to more controls using Camera2’s CaptureRequest. For example, the following code will make the image negative.

Camera2Interop.Extender ext = new Camera2Interop.Extender<>(imageAnalysisBuilder);
ext.setCaptureRequestOption(CaptureRequest.CONTROL_EFFECT_MODE,CaptureRequest.CONTROL_EFFECT_MODE_NEGATIVE);

Make the App More Usable

More functions can be added like preferences, view finder, scan history, and making a beep if a barcode is found. You can find an example here. We will not discuss these in this post.

Source Code

https://github.com/xulihang/dynamsoft-samples/tree/main/dbr/Android/CameraXMinimum

References

  1. https://developer.android.com/training/camera2 

  2. https://developer.android.com/training/camerax