The Quickest Way to Create an Android QR Code Scanner
A QR code scanner application is fundamentally composed of two key components: the camera preview and the QR code scanning functionality. While there are numerous QR code scanner apps available for download on the Google Play Store, building your own can be a more rewarding and educational experience. This article will guide you through the fastest way to create an Android QR code scanner, walking you through the step-by-step implementation of the camera preview and the seamless integration of a QR code scanning SDK.
This article is Part 4 in a 7-Part Series.
- Part 1 - High-Speed Barcode and QR Code Detection on Android Using Camera2 API
- Part 2 - Optimizing Android Barcode Scanning with NDK and JNI C++
- Part 3 - Choosing the Best Tool for High-Density QR Code Scanning on Android: Google ML Kit vs. Dynamsoft Barcode SDK
- Part 4 - The Quickest Way to Create an Android QR Code Scanner
- Part 5 - Real-time QR Code Recognition on Android with YOLO and Dynamsoft Barcode Reader
- Part 6 - Accelerating Android QR Code Detection with TensorFlow Lite
- Part 7 - Effortlessly Recognize US Driver's Licenses in Android Apps
Prerequisites
-
Dynamsoft Barcode Reader Trial License
To use the Dynamsoft Barcode Reader, you’ll need a trial license key. Integrate it into your Java code as follows:
BarcodeReader.initLicense( "LICENSE-KEY", new DBRLicenseVerificationListener() { @Override public void DBRLicenseVerificationCallback(boolean isSuccessful, Exception e) { } });
-
Dynamsoft Maven Repository
Add the Dynamsoft Maven repository to your project by modifying the
settings.gradle
file:dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { ... maven{url "https://download2.dynamsoft.com/maven/aar"} } }
-
Camera Preview SDK
-
CameraX
Google’s CameraX API simplifies camera app development, making it more accessible for beginners compared to the complex Android Camera2 API. To get started with CameraX, refer to the codelab tutorial.
Add the CameraX dependencies in your
build.gradle
: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 v2.3.0
Dynamsoft Camera Enhancer is a powerful wrapper around the Android Camera2 API, offering advanced features like frame filtering for enhanced image quality. We’ll compare its performance with CameraX in this project.
Include the following dependency in your
build.gradle
:dependencies { ... implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.3.0@aar' }
-
-
Dynamsoft Barcode Reader v9.x for QR Code Scanning
The Dynamsoft Barcode Reader SDK supports a wide range of barcode formats, including both linear and 2D barcodes. To enable QR code scanning in your app, add this dependency to your
app/build.gradle
:dependencies { ... implementation 'com.dynamsoft:dynamsoftbarcodereader:9.6.40@aar' }
Creating an Android Camera Preview in 5 Minutes
Implementing Camera Preview with CameraX in Three Simple Steps
The official CameraX tutorial is provided in Kotlin, but here we’ll show you how to achieve the same in Java.
Step 1: Add Camera Permissions
In your AndroidManifest.xml
, declare the necessary camera permissions:
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
Step 2: Create the UI Layout for CameraX Preview
Define the layout in XML, including a PreviewView
for the CameraX preview:
<?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>
Step 3: Check and Request Camera Permissions
Ensure the app has the necessary permissions to access the camera, and if not, request them:
@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;
}
Step 4: Start the Camera Preview
Finally, initialize and 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()));
}
Implementing Camera Preview with Dynamsoft Camera Enhancer in Two Simple Steps
Dynamsoft Camera Enhancer allows you to implement camera preview with less code compared to CameraX while providing the same functionality.
Step 1: Create the UI Layout with the DCE Preview View
Define the layout in XML, including the DCECameraView
for the camera preview:
<?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>
Step 2: Start the Camera Preview
Initialize and start the camera preview in the DceActivity
:
@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();
}
}
Combining CameraX and Dynamsoft Camera Enhancer
To allow users to choose between CameraX and Dynamsoft Camera Enhancer, create an entry activity that launches the respective activity based on the user’s choice:
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 Your Android Camera into a QR Code Scanner
To enable QR code scanning, you need to continuously capture camera preview frames and pass them to a QR code detector.
Setting the Camera Frame Callback
With CameraX, you can use the ImageAnalysis
class to receive 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 simplifies this process, providing a callback function similar to the one used in Android Camera API:
public class DceActivity extends AppCompatActivity implements DCEFrameListener {
@Override
public void frameOutputCallback(DCEFrame dceFrame, long l) {
// image processing
}
}
Decoding QR Codes
To decode QR codes, the process differs slightly between CameraX and Dynamsoft Camera Enhancer:
-
With CameraX: Convert the
ByteBuffer
to abyte[]
and then call thedecodeBuffer()
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(); });
-
With Dynamsoft Camera Enhancer: Convert the
DCEFrame
to aBitmap
and then call thedecodeBufferedImage()
method:public void frameOutputCallback(DCEFrame dceFrame, long l) { TextResult[] results = null; try { results = reader.decodeBufferedImage(dceFrame.toBitmap(), ""); } catch (BarcodeReaderException e) { e.printStackTrace(); } }
Enhancing Frame Quality with Zoom and Torch Features
The accuracy of QR code recognition is heavily influenced by the quality of the input image. If the QR code is too small, zooming in can help by enlarging the image. If the image is too dark, turning on the torch can brighten it, improving detection accuracy. Both CameraX and Dynamsoft Camera Enhancer fully support these camera controls.
Implementing Android Camera Zoom
To enable zoom functionality, we’ll use a pinch gesture. The first step is to create a gesture detector and override 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);
}
When a pinch gesture is detected, the scale factor is used to adjust the zoom ratio.s
Setting Camera Zoom Ratio with CameraX
if (camera != null) {
camera.getCameraControl().setZoomRatio(ratio);
}
Setting Camera Zoom Ratio with Dynamsoft Camera Enhancer
try {
cameraEnhancer.setZoom(ratio);
} catch (CameraEnhancerException e) {
e.printStackTrace();
}
Implementing Android Camera Torch
To automatically manage the torch, we’ll monitor the ambient light levels using the device’s 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;
}
}
Toggling Camera Torch with CameraX
if (camera != null) camera.getCameraControl().enableTorch(status);
Toggling 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();
}
}