Android Barcode Scanner with CameraX
You can build an Android barcode scanner with CameraX by combining its preview and image-analysis use cases with Dynamsoft Barcode Reader for decoding: CameraX delivers the camera frames, and the Capture Vision engine (com.dynamsoft:barcodereaderbundle:11.6.2000) decodes barcodes from the NV21 frame buffer.
Camera control and barcode reading are the two essential parts of a barcode scanner, and Dynamsoft Barcode Reader provides an easy-to-use Android library for the reading part.
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.

What you’ll build: a CameraX barcode scanner app with two activities: a launcher activity that requests the camera permission, and a camera activity that binds Preview and ImageAnalysis to the lifecycle, decodes every frame with CaptureVisionRouter, and prints the decoded barcode text under the preview.
Key Takeaways
- A CameraX barcode scanner needs two use cases:
Previewfor the viewfinder andImageAnalysisfor decoding; both should share the same target resolution. - The default analyzer image is YUV: wrap the first plane’s bytes in an
ImageDatawithEnumImagePixelFormat.IPF_NV21and pass it tocvr.capture(imageData, EnumPresetTemplate.PT_READ_BARCODES). CapturedResult.getItems()returnsBarcodeResultItementries with the decoded text and format; update the UI on the main thread because the analyzer runs on a background executor.- The CameraX
CameraControlAPI covers torch, zoom, and focus;Camera2Interop.Extenderunlocks extra Camera2 capture requests such as negative color effects.
Common Developer Questions
Which CameraX use cases do I need for a barcode scanner?
Preview to display the camera feed and ImageAnalysis to decode the frames. Both are bound to the lifecycle in a single UseCaseGroup so they share one camera session.
How do I decode a barcode from a CameraX frame?
In the ImageAnalysis.Analyzer, copy the Y plane of the ImageProxy into a byte array, fill an ImageData with the width, height, stride, and IPF_NV21 format, and call cvr.capture(imageData, EnumPresetTemplate.PT_READ_BARCODES). The returned CapturedResult holds the decoded BarcodeResultItem array.
Can I read the barcode text from a rotated frame?
CameraX delivers the raw sensor image in ImageProxy, and getImageInfo().getRotationDegrees() tells you the rotation. Decoding works on the raw YUV data; use setOutputImageRotationEnabled or rotate the bitmap only when you need an upright image for other processing.
Why does the analyzer need the full target resolution?
Small barcodes occupy very few pixels at low resolution. Sharing a high target resolution (for example 720p or 1080p) between Preview and ImageAnalysis keeps the module size large enough for reliable decoding.
Prerequisites
- Android Studio with the Android SDK (API level 21 or higher, because CameraX requires Android 5.0+).
- An Android device or emulator with a camera.
- A Dynamsoft license key - the sample uses a time-limited trial key that requires a network connection on first use.
Get a 30-day free trial license
Create a New Project
- 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. -
Add Dynamsoft Barcode Reader by adding the following to
build.gradle:implementation 'com.dynamsoft:barcodereaderbundle:11.6.2000'You also need to add Dynamsoft’s repo:
repositories { maven { url "https://download2.dynamsoft.com/maven/aar" } } -
Add CameraX by adding the following to
build.gradle:def camerax_version = '1.3.4' // CameraX core library using camera2 implementation implementation "androidx.camera:camera-camera2:$camerax_version" // CameraX Lifecycle Library implementation "androidx.camera:camera-lifecycle:$camerax_version" // CameraX View class implementation "androidx.camera:camera-view:$camerax_version"
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
-
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" /> -
Init and enable preview and image analysis use cases
Dynamsoft Barcode Reader is used to read barcodes from the image buffer. The sample initializes the license in
MainActivitywith a trial key from the prerequisites.private CaptureVisionRouter cvr; @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(){ String license = "LICENSE-KEY"; LicenseManager.initLicense(license, getApplicationContext(), (isSuccess, error) -> { if (!isSuccess) { Log.e("DBR", "InitLicense Error: " + error); }else{ Log.d("DBR","license valid"); } }); cvr = new CaptureVisionRouter(getApplicationContext()); } @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) { CapturedResult capturedResult = 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(); imageData.bytes = bytes; imageData.width = image.getWidth(); imageData.height = image.getHeight(); imageData.stride = nRowStride*nPixelStride; imageData.format = EnumImagePixelFormat.IPF_NV21; capturedResult = cvr.capture(imageData, EnumPresetTemplate.PT_READ_BARCODES); CapturedResultItem[] results = capturedResult.getItems(); StringBuilder sb = new StringBuilder(); sb.append("Found ").append(results.length).append(" barcode(s):\n"); if (results != null) { for (CapturedResultItem result:results) { if (result instanceof BarcodeResultItem) { BarcodeResultItem barcodeResult = (BarcodeResultItem) result; sb.append(barcodeResult.getText()); 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); }Here are some things to notice:
- Preview and ImageAnalysis should share the same target resolution.
- The resolution is set using setTargetResolution. The Size should match the screen orientation. The resolution can also be set using setTargetAspectRatio.
- 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.
- 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);
Common Issues & Edge Cases
- No barcode is ever found in the analyzer. The analyzer runs on a background executor, so keep UI updates inside
runOnUiThread. Also verify the license initialized successfully, otherwise results are masked. - Distorted preview or analysis image. The
Sizepassed tosetTargetResolutionmust match the screen orientation (portrait 720x1280, landscape 1280x720) andPreviewandImageAnalysismust use the same resolution. - Analyzer buffer looks wrong.
image.getPlanes()[0]is the Y plane of a YUV image; the sample computes the stride asrowStride * pixelStridewhen building theImageData. If you need RGB bitmaps, convert the YUV frame with a helper such as Google’sYuvToRgbConverter. - Camera features missing on some devices. Zoom and focus capabilities depend on the hardware. Check
camera.getCameraInfo().getZoomState()and handle devices without autofocus or zoom support.
Source Code
Get the complete sample project source code on GitHub