The Quickest Way to Create an Android QR Code Scanner

A QR code scanner application primarily consists of two parts: camera preview and QR code scanning. There are many Android QR code scanner apps downloadable in the Google Play Store. However, it is more interesting to create a QR code scanner by yourself than to use an existing one. This article aims to reveal the quickest way to create an Android QR code scanner. You will see how to step by step implement camera preview, as well as how to integrate QR code scanning SDK.

Prerequisites

The following Android libraries are required in order to implement the QR code scanner. You can feel free to replace them with your own libraries.

  • Camera Preview SDK

    • CameraX

      Since Android Camera2 API is extremely complicated for beginners, Google released CameraX to simplify camera app development. The codelab tutorial is a good starting point to learn CameraX.

      Installation

      In AndroidManifest.xml, add the camera permission:

        <uses-feature android:name="android.hardware.camera.any" />
        <uses-permission android:name="android.permission.CAMERA" />
      

      In app/build.gradle, add the dependency:

        dependencies {
            ...
            def camerax_version = "1.0.1"
            implementation "androidx.camera:camera-camera2:$camerax_version"
            implementation "androidx.camera:camera-lifecycle:$camerax_version"
            implementation "androidx.camera:camera-view:1.0.0-alpha27"
        }
      
    • Dynamsoft Camera Enhancer

      Similar to CameraX, Dynamsoft Camera Enhancer is also a wrapper for Android Camera2 API. In addition to basic camera capabilities, it features frame filtering for better image quality. We use Dynamsoft Camera Enhancer to make a comparison with CameraX.

      Installation

      In settings.gradle, add the custom maven repository:

        dependencyResolutionManagement {
            repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
            repositories {
                ...
                maven{url "https://download.dynamsoft.com/maven/dce/aar"}
            }
        }
      

      In app/build.gradle, add the dependency:

        dependencies {
            ...
            implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.1.0@aar'
        }
      
  • QR Code Scanning SDK

    • Dynamsoft Barcode Reader

      A barcode SDK that supports all mainstream linear barcode and 2D barcode formats.

      Installation

      In settings.gradle, add the custom maven repository:

        dependencyResolutionManagement {
            repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
            repositories {
                ...
                maven{url "https://download.dynamsoft.com/maven/dbr/aar"}
            }
        }
      

      In app/build.gradle, add the dependency:

        dependencies {
            ...
            implementation 'com.dynamsoft:dynamsoftbarcodereader:8.9.0@aar'
        }
      

      You also need a license key to activate the barcode SDK.

Creating Android Camera Preview within 5 Minutes

Three Steps to Implement Camera Preview with CameraX

The official CameraX tutorial is written in Kotlin. Here we use Java.

  1. Create the UI layout which contains the CameraX preview view:

     <?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=".CameraXActivity">
        
         <androidx.camera.view.PreviewView
             android:id="@+id/camerax_viewFinder"
             android:layout_width="match_parent"
             android:layout_height="match_parent" />
        
     </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. Check and request camera permissions:

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.camerax_main);
         previewView = findViewById(R.id.camerax_viewFinder);
    
         if (!CameraUtils.allPermissionsGranted(this)) {
             CameraUtils.getRuntimePermissions(this);
         } else {
             startCamera();
         }
     }
    
     private static String[] getRequiredPermissions(Context context) {
         try {
             PackageInfo info =
                     context.getPackageManager()
                             .getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
             String[] ps = info.requestedPermissions;
             if (ps != null && ps.length > 0) {
                 return ps;
             } else {
                 return new String[0];
             }
         } catch (Exception e) {
             return new String[0];
         }
     }
    
     public static boolean allPermissionsGranted(Context context) {
         for (String permission : getRequiredPermissions(context)) {
             if (!isPermissionGranted(context, permission)) {
                 return false;
             }
         }
         return true;
     }
    
     public static void getRuntimePermissions(Activity activity) {
         List<String> allNeededPermissions = new ArrayList<>();
         for (String permission : getRequiredPermissions(activity)) {
             if (!isPermissionGranted(activity, permission)) {
                 allNeededPermissions.add(permission);
             }
         }
    
         if (!allNeededPermissions.isEmpty()) {
             ActivityCompat.requestPermissions(
                     activity, allNeededPermissions.toArray(new String[0]), PERMISSION_REQUESTS);
         }
     }
    
     private static boolean isPermissionGranted(Context context, String permission) {
         if (ContextCompat.checkSelfPermission(context, permission)
                 == PackageManager.PERMISSION_GRANTED) {
             Log.i(TAG, "Permission granted: " + permission);
             return true;
         }
         Log.i(TAG, "Permission NOT granted: " + permission);
         return false;
     }
    
  3. Start the camera preview:

     private void startCamera() {
         ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
                 ProcessCameraProvider.getInstance(getApplication());
         cameraProviderFuture.addListener(
                 () -> {
                     try {
                         ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
    
                         Preview.Builder builder = new Preview.Builder();
                         Preview previewUseCase = builder.build();
                         previewUseCase.setSurfaceProvider(previewView.getSurfaceProvider());
                         CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
                         cameraProvider.unbindAll();
                         cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase);
                     } catch (ExecutionException | InterruptedException e) {
                         Log.e(TAG, "Unhandled exception", e);
                     }
                 },
                 ContextCompat.getMainExecutor(getApplication()));
     }
    

Two Steps to Implement Camera Preview with Dynamsoft Camera Enhancer

Using Dynamsoft Camera Enhancer, you can write less code than using CameraX to implement the same functionality.

  1. Create the UI layout which contains the DCE preview view.

     <?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=".CameraXActivity">
        
         <com.dynamsoft.dce.DCECameraView
             android:id="@+id/dce_viewFinder"
             android:layout_width="match_parent"
             android:layout_height="match_parent" />
        
     </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. Start the camera preview:

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.dce_main);
         previewView = findViewById(R.id.dce_viewFinder);
    
         cameraEnhancer = new CameraEnhancer(this);
         cameraEnhancer.setCameraView(previewView);
         cameraEnhancer.addListener(this);
     }
    
     @Override
     protected void onResume() {
         super.onResume();
         try {
             cameraEnhancer.open();
         } catch (CameraEnhancerException e) {
             e.printStackTrace();
         }
     }
    
     @Override
     protected void onPause() {
         super.onPause();
         try {
             cameraEnhancer.close();
         } catch (CameraEnhancerException e) {
             e.printStackTrace();
         }
     }
    

All in One

We create an entry activity to launch the CameraX and Dynamsoft Camera Enhancer respectively:

package com.example.qrcodescanner;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class EntryChoiceActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.entry_choice);

        findViewById(R.id.camerax_entry_point).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(EntryChoiceActivity.this, CameraXActivity.class);
                startActivity(intent);
            }
        });

        findViewById(R.id.dce_entry_point).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(EntryChoiceActivity.this, DceActivity.class);
                startActivity(intent);
            }
        });
    }
}

Turning Android Camera into QR Code Scanner

To scan QR code, we need to continuously get the camera preview frames and pass the frames to QR code detector.

How to Set the Camera Frame Callback

When using CameraX, we can use ImageAnalysis class to receive the camera frames:

ImageAnalysis analysisUseCase = new ImageAnalysis.Builder().build();
analysisUseCase.setAnalyzer(cameraExecutor,
        imageProxy -> {
            // image processing
            // Must call close to keep receiving frames.
            imageProxy.close();
        });
cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase, analysisUseCase);

In contrast, Dynamsoft Camera Enhancer is much easier. The callback function looks like the one used in Android Camera1:

public class DceActivity extends AppCompatActivity implements DCEFrameListener {
    @Override
    public void frameOutputCallback(DCEFrame dceFrame, long l) {
        // image processing
    }
}

The data types returned by their callback functions are different. Data type conversion is required for later use.

Decoding QR Code

With CameraX, we firstly convert ByteBuffer to byte[] and then call the decodeBuffer() method:

analysisUseCase.setAnalyzer(cameraExecutor,
imageProxy -> {
    TextResult[] results = null;
    ByteBuffer buffer = imageProxy.getPlanes()[0].getBuffer();
    int nRowStride = imageProxy.getPlanes()[0].getRowStride();
    int nPixelStride = imageProxy.getPlanes()[0].getPixelStride();
    int length = buffer.remaining();
    byte[] bytes = new byte[length];
    buffer.get(bytes);
    try {
        results = reader.decodeBuffer(bytes, imageProxy.getWidth(), imageProxy.getHeight(), nRowStride * nPixelStride, EnumImagePixelFormat.IPF_NV21, "");
    } catch (BarcodeReaderException e) {
        e.printStackTrace();
    }

    // Must call close to keep receiving frames.
    imageProxy.close();
});

Whereas with Dynamsoft Camera Enhancer, we get Bitmap from the DCEFrame and then call the decodeBufferedImage() method:

public void frameOutputCallback(DCEFrame dceFrame, long l) {
    TextResult[] results = null;
    try {
        results = reader.decodeBufferedImage(dceFrame.toBitmap(), "");
    } catch (BarcodeReaderException e) {
        e.printStackTrace();
    }
}

Android QR code scanner

Using Zoom and Torch to Boost the Frame Quality

The recognition accuracy is always affected by the input image quality. If the QR code is too small, we can zoom in the camera to scale up the image. If the input image is too dark, we can turn on the torch to brighten the image. Both of CameraX and Dynamsoft Camera Enhancer have completely supported the camera control.

Android Camera Zoom

We use finger pinch gesture to trigger the zoom. Thus, the first step is to create the gesture detector and take over the onTouchEvent() method:

public class ZoomController {
    public final static String TAG = "ZoomController";
    private float currentFactor = 1.0f;
    private float minZoomRatio = 1.0f, maxZoomRatio = 1.0f;
    private ZoomStatus zoomStatus;
    private ScaleGestureDetector scaleGestureDetector;
    private ScaleGestureDetector.OnScaleGestureListener scaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            Log.i(TAG, "onScale: " + detector.getScaleFactor());
            currentFactor = detector.getScaleFactor() * currentFactor;
            if (currentFactor < minZoomRatio) currentFactor = minZoomRatio;
            if (currentFactor > maxZoomRatio) currentFactor = maxZoomRatio;

            if (zoomStatus != null) {
                zoomStatus.onZoomChange(currentFactor);
            }

            return true;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
        }
    };

    public ZoomController(Activity activity) {
        scaleGestureDetector = new ScaleGestureDetector(activity, scaleGestureListener);
    }

    public interface ZoomStatus {
        void onZoomChange(float ratio);
    }

    public void addListener(ZoomStatus zoomStatus) {
        this.zoomStatus = zoomStatus;
    }

    public void initZoomRatio(float minZoomRatio, float maxZoomRatio) {
        this.minZoomRatio = minZoomRatio;
        this.maxZoomRatio = maxZoomRatio;
    }

    public boolean onTouchEvent(MotionEvent event) {
        return scaleGestureDetector.onTouchEvent(event);
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    zoomController.onTouchEvent(event);
    return super.onTouchEvent(event);
}

As the gesture is detected, we get the scale factor and take it as the zoom ratio.

Set camera zoom ratio with CameraX

if (camera != null) {
    camera.getCameraControl().setZoomRatio(ratio);
}

Set camera zoom ratio with Dynamsoft Camera Enhancer

try {
    cameraEnhancer.setZoom(ratio);
} catch (CameraEnhancerException e) {
    e.printStackTrace();
}

Android Camera Torch

To turn on the torch automatically, we monitor the light value returned by the light sensor.

public class AutoTorchController implements SensorEventListener {
    public final static String TAG = "AutoTorchController";
    private SensorManager sensorManager;
    private TorchStatus torchStatus;

    public interface TorchStatus {
        void onTorchChange(boolean status);
    }

    public AutoTorchController(Activity activity) {
        sensorManager = (SensorManager)activity.getSystemService(SENSOR_SERVICE);
    }

    public void onStart() {
        Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        if(lightSensor != null){
            sensorManager.registerListener(
                    this,
                    lightSensor,
                    SensorManager.SENSOR_DELAY_NORMAL);

        }
    }

    public void onStop() {
        sensorManager.unregisterListener(this);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if(event.sensor.getType() == Sensor.TYPE_LIGHT){
            if (event.values[0] < 20) {
                if (torchStatus != null) torchStatus.onTorchChange(true);
            }
            else {
                if (torchStatus != null) torchStatus.onTorchChange(false);
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    public void addListener(TorchStatus torchStatus) {
        this.torchStatus = torchStatus;
    }
}

Toggle camera torch with CameraX

if (camera != null) camera.getCameraControl().enableTorch(status);

Toggle camera torch with Dynamsoft Camera Enhancer

if (status) {
    try {
        cameraEnhancer.turnOnTorch();
    } catch (CameraEnhancerException e) {
        e.printStackTrace();
    }
}
else {
    try {
        cameraEnhancer.turnOffTorch();
    } catch (CameraEnhancerException e) {
        e.printStackTrace();
    }
}

Demo Video of Android QR Code Scanner

Source Code