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.
-
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>
-
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; }
-
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.
-
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>
-
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();
}
}
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
https://github.com/yushulx/android-camera2-preview-qr-code-scanner