US driver's licenses store personal information using PDF417 barcode symbology according to the American Association of Motor Vehicle Administrators (AAMVA) specification. This comprehensive guide shows you how to build a production-ready Android driver's license scanner app with camera resolution options (720P/1080P) to compare scanning performance between Dynamsoft Capture Vision and Google ML Kit.
Demo Video: Driver's License Scanner App on Android
Features of This Driver's License Scanner App
Key Features:
- Dual Scanning Engines: Compare Dynamsoft and Google ML Kit side-by-side
- Resolution Selection: Switch between 720P and 1080P for performance testing
- Real-time Resolution Display: See actual camera resolution on both engines
- Modern Material Design UI: Beautiful home screen with resolution settings
- Complete AAMVA Data Extraction: Parse all driver's license fields including name, address, DOB, license number, and more
Prerequisites
- Android Studio (latest version recommended)
- Android device with camera (Android 5.0+)
- 30-day Trial License for Dynamsoft Barcode Reader
Sample Driver's License Image for Testing
Use this sample driver's license image to test the barcode scanning functionality:
Building the Driver's License Scanner with Dynamsoft Barcode Reader
Step 1: Android Project Configuration
-
In your project's
build.gradlefile, add the Dynamsoft Maven repository:
allprojects { repositories { google() mavenCentral() maven { url "https://download2.dynamsoft.com/maven/aar" } } } -
In the module's
build.gradlefile, add these dependencies:
implementation 'com.dynamsoft:barcodereaderbundle:11.2.5000'
Step 2: License Activation
Initialize the Dynamsoft license in your MainActivity.java:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
LicenseManager.initLicense("YOUR-LICENSE-KEY", this, (isSuccessful, error) -> {
if (!isSuccessful) {
error.printStackTrace();
runOnUiThread(() -> ((TextView) findViewById(R.id.tv_license_error))
.setText("License initialization failed: " + error.getMessage()));
}
});
}
...
}
Step 3: Creating the Home Screen with Resolution Settings
Create fragment_selection.xml with a modern Material Design interface:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Driver License Scanner"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="#212121"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Choose your scanning engine"
android:textSize="16sp"
android:textColor="#757575"
android:layout_marginBottom="32dp"/>
<!-- Resolution Selection Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_manage"
android:tint="#1976D2"
android:layout_marginEnd="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Camera Resolution"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#212121"/>
</LinearLayout>
<RadioGroup
android:id="@+id/radio_group_resolution"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#F5F5F5"
android:padding="4dp">
<RadioButton
android:id="@+id/radio_720p"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="720P"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/radio_text_color"
android:gravity="center"
android:button="@null"
android:background="@drawable/radio_selector"
android:layout_marginEnd="4dp"/>
<RadioButton
android:id="@+id/radio_1080p"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="1080P"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/radio_text_color"
android:gravity="center"
android:button="@null"
android:background="@drawable/radio_selector"
android:layout_marginStart="4dp"/>
</RadioGroup>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Engine Selection Cards -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scanning Engines"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#212121"
android:layout_marginBottom="16dp"
android:layout_gravity="start"/>
<!-- Dynamsoft Card -->
<androidx.cardview.widget.CardView
android:id="@+id/card_dynamsoft"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/circle_background_blue"
android:gravity="center"
android:layout_marginEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="D"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#FFFFFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dynamsoft"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#212121"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Commercial-grade barcode scanner SDK"
android:textSize="14sp"
android:textColor="#757575"/>
</LinearLayout>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_send"
android:tint="#BDBDBD"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Google Card -->
<androidx.cardview.widget.CardView
android:id="@+id/card_google"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/circle_background_green"
android:gravity="center"
android:layout_marginEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="G"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#FFFFFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Google Mobile Vision"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#212121"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Free on-device vision API"
android:textSize="14sp"
android:textColor="#757575"/>
</LinearLayout>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_send"
android:tint="#BDBDBD"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
Step 4: Creating the ViewModel for Shared State
Create MainViewModel.java to share data across fragments:
package com.dynamsoft.dcv.driverslicensescanner;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
public String[] results;
public String parsedText;
// 0: 720P (1280x720), 1: 1080P (1920x1080)
public int resolutionIndex = 0;
public void reset() {
results = null;
parsedText = null;
}
}
Step 5: Implementing the Selection Fragment
Create SelectionFragment.java to handle resolution and engine selection:
package com.dynamsoft.dcv.driverslicensescanner.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
import com.dynamsoft.dcv.driverslicensescanner.R;
public class SelectionFragment extends Fragment {
private MainViewModel viewModel;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_selection, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
// Resolution selection
android.widget.RadioGroup radioGroup = view.findViewById(R.id.radio_group_resolution);
if (viewModel.resolutionIndex == 0) {
radioGroup.check(R.id.radio_720p);
} else {
radioGroup.check(R.id.radio_1080p);
}
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.radio_720p) {
viewModel.resolutionIndex = 0;
} else if (checkedId == R.id.radio_1080p) {
viewModel.resolutionIndex = 1;
}
});
// Engine selection
CardView cardDynamsoft = view.findViewById(R.id.card_dynamsoft);
CardView cardGoogle = view.findViewById(R.id.card_google);
cardDynamsoft.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(R.id.action_SelectionFragment_to_ScannerFragment);
});
cardGoogle.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(R.id.action_SelectionFragment_to_GoogleScannerFragment);
});
}
}
Step 6: Creating the Scanner Fragment with Resolution Display
-
Create a new layout file named
fragment_scanner.xmlin thelayoutfolder:
<?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=".fragments.ScannerFragment"> <com.dynamsoft.dce.CameraView android:id="@+id/camera_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <TextView android:id="@+id/tv_resolution" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:textColor="@android:color/white" android:textSize="16sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>The layout includes a
CameraViewfor displaying the camera preview and aTextViewfor showing the camera resolution. -
Place a
drivers-license.jsontemplate file in theassetsfolder. This template defines how to decode the PDF417 barcode and extract driver's license information. Below is an example of the template:
{ "BarcodeFormatSpecificationOptions" : [ { "Name" : "bfs_pdf_417", "BarcodeBytesLengthRangeArray" : [ { "MaxValue" : 2147483647, "MinValue" : 0 } ], "BarcodeFormatIds" : [ "BF_DEFAULT", "BF_POSTALCODE", "BF_PHARMACODE", "BF_NONSTANDARD_BARCODE", "BF_DOTCODE" ], "BarcodeHeightRangeArray" : null, "BarcodeTextLengthRangeArray" : [ { "MaxValue" : 2147483647, "MinValue" : 0 } ], "MinResultConfidence" : 30, "MirrorMode" : "MM_NORMAL", "PartitionModes" : [ "PM_WHOLE_BARCODE", "PM_ALIGNMENT_PARTITION" ] } ], "BarcodeReaderTaskSettingOptions" : [ { "Name" : "pdf_417_task", "BarcodeColourModes" : [ { "LightReflection" : 1, "Mode" : "BICM_DARK_ON_LIGHT" } ], "BarcodeFormatSpecificationNameArray" : [ "bfs_pdf_417" ], "DeblurModes" : null, "LocalizationModes" : [ { "Mode" : "LM_CONNECTED_BLOCKS" }, { "Mode" : "LM_LINES" }, { "Mode" : "LM_STATISTICS" } ], "MaxThreadsInOneTask" : 1, "SectionImageParameterArray" : [ { "ContinueWhenPartialResultsGenerated" : 1, "ImageParameterName" : "ip_localize_barcode", "Section" : "ST_REGION_PREDETECTION" }, { "ContinueWhenPartialResultsGenerated" : 1, "ImageParameterName" : "ip_localize_barcode", "Section" : "ST_BARCODE_LOCALIZATION" }, { "ContinueWhenPartialResultsGenerated" : 1, "ImageParameterName" : "ip_decode_barcode", "Section" : "ST_BARCODE_DECODING" } ] } ], "CaptureVisionTemplates" : [ { "Name" : "ReadPDF417", "ImageROIProcessingNameArray" : [ "roi_pdf_417" ], "ImageSource" : "", "MaxParallelTasks" : 4, "MinImageCaptureInterval" : 0, "OutputOriginalImage" : 0, "SemanticProcessingNameArray": [ "sp_pdf_417" ], "Timeout" : 10000 } ], "GlobalParameter" : { "MaxTotalImageDimension" : 0 }, "ImageParameterOptions" : [ { "Name" : "ip_localize_barcode", "BinarizationModes" : [ { "BinarizationThreshold" : -1, "BlockSizeX" : 71, "BlockSizeY" : 71, "EnableFillBinaryVacancy" : 0, "GrayscaleEnhancementModesIndex" : -1, "Mode" : "BM_LOCAL_BLOCK", "ThresholdCompensation" : 10 } ], "GrayscaleEnhancementModes" : [ { "Mode" : "GEM_GENERAL" } ] }, { "Name" : "ip_decode_barcode", "ScaleDownThreshold" : 99999 }, { "Name": "ip_recognize_text", "TextDetectionMode": { "Mode": "TTDM_LINE", "Direction": "HORIZONTAL", "CharHeightRange": [ 20, 1000, 1 ], "Sensitivity": 7 } } ], "TargetROIDefOptions" : [ { "Name" : "roi_pdf_417", "TaskSettingNameArray" : [ "pdf_417_task" ] } ], "CharacterModelOptions": [ { "Name" : "NumberLetter" } ], "SemanticProcessingOptions": [ { "Name": "sp_pdf_417", "ReferenceObjectFilter": { "ReferenceTargetROIDefNameArray": [ "roi_pdf_417" ] }, "TaskSettingNameArray": [ "dcp_pdf_417" ] } ], "CodeParserTaskSettingOptions": [ { "Name": "dcp_pdf_417", "CodeSpecifications": ["AAMVA_DL_ID","AAMVA_DL_ID_WITH_MAG_STRIPE","SOUTH_AFRICA_DL"] } ] } -
Implement the barcode scanning functionality with camera resolution control in
ScannerFragment.java:
package com.dynamsoft.dcv.driverslicensescanner.fragments; import android.app.AlertDialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.dynamsoft.core.basic_structures.CompletionListener; import com.dynamsoft.cvr.CaptureVisionRouter; import com.dynamsoft.cvr.CaptureVisionRouterException; import com.dynamsoft.cvr.CapturedResultReceiver; import com.dynamsoft.dbr.DecodedBarcodesResult; import com.dynamsoft.dce.CameraEnhancer; import com.dynamsoft.dce.EnumResolution; import com.dynamsoft.dcp.ParsedResult; import com.dynamsoft.dcv.driverslicensescanner.FileUtil; import com.dynamsoft.dcv.driverslicensescanner.MainViewModel; import com.dynamsoft.dcv.driverslicensescanner.ParseUtil; import com.dynamsoft.dcv.driverslicensescanner.R; import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentScannerBinding; import java.util.Locale; public class ScannerFragment extends Fragment { private static final String TEMPLATE_ASSETS_FILE_NAME = "drivers-license.json"; private static final String TEMPLATE_READ_PDF417 = "ReadPDF417"; private FragmentScannerBinding binding; private CameraEnhancer mCamera; private CaptureVisionRouter mRouter; private MainViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentScannerBinding.inflate(inflater, container, false); viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); viewModel.reset(); mCamera = new CameraEnhancer(binding.cameraView, getViewLifecycleOwner()); if (viewModel.resolutionIndex == 0) { mCamera.setResolution(EnumResolution.RESOLUTION_720P); } else { mCamera.setResolution(EnumResolution.RESOLUTION_1080P); } if (mRouter == null) { initCaptureVisionRouter(); } try { mRouter.setInput(mCamera); } catch (CaptureVisionRouterException e) { e.printStackTrace(); } return binding.getRoot(); } @Override public void onResume() { super.onResume(); mCamera.open(); try { android.util.Size size = mCamera.getResolution(); if (size != null) { binding.tvResolution.setText("Resolution: " + size.getWidth() + "x" + size.getHeight()); } } catch (Exception e) { e.printStackTrace(); } mRouter.startCapturing(TEMPLATE_READ_PDF417, new CompletionListener() { @Override public void onSuccess() { } @Override public void onFailure(int errorCode, String errorString) { requireActivity().runOnUiThread(() -> showDialog("Error", String.format(Locale.getDefault(), "ErrorCode: %d %nErrorMessage: %s", errorCode, errorString))); } }); } @Override public void onPause() { super.onPause(); mCamera.close(); mRouter.stopCapturing(); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } private void initCaptureVisionRouter() { mRouter = new CaptureVisionRouter(requireContext()); try { String template = FileUtil.readAssetFileToString(requireContext(), TEMPLATE_ASSETS_FILE_NAME); mRouter.initSettings(template); } catch (CaptureVisionRouterException e) { e.printStackTrace(); } mRouter.addResultReceiver(new CapturedResultReceiver() { @Override public void onDecodedBarcodesReceived(DecodedBarcodesResult result) { if (result.getItems().length > 0) { viewModel.parsedText = result.getItems()[0].getText(); } } @Override public void onParsedResultsReceived(ParsedResult result) { if (result.getItems().length > 0) { String[] displayStrings = ParseUtil.parsedItemToDisplayStrings(result.getItems()[0]); if (displayStrings == null || displayStrings.length <= 1) { showParsedText(); return; } viewModel.results = displayStrings; requireActivity().runOnUiThread(() -> NavHostFragment.findNavController(ScannerFragment.this) .navigate(R.id.action_ScannerFragment_to_ResultFragment)); mRouter.stopCapturing(); } else { showParsedText(); } } }); } private void showParsedText() { if (viewModel.parsedText != null && !viewModel.parsedText.isEmpty()) { requireActivity().runOnUiThread(() -> { if (binding != null) { // Display raw barcode text if parsing fails } }); } } private void showDialog(String title, String message) { new AlertDialog.Builder(requireContext()) .setCancelable(true) .setPositiveButton("OK", null) .setTitle(title) .setMessage(message) .show(); } }Key Implementation Details:
-
Resolution Control: Sets camera resolution using
EnumResolution.RESOLUTION_720PorRESOLUTION_1080Pbased on user selection -
Template-Based Scanning: Uses
drivers-license.jsontemplate file for optimized PDF417 recognition - CapturedResultReceiver: Anonymous inner class receives both barcode and parsed results
-
Code Parsing: Uses
ParseUtilto format driver's license data fromParsedResultItem -
Resolution Display: Shows actual camera resolution in TextView using
mCamera.getResolution() -
Lifecycle Management: Properly opens/closes camera in
onResume()/onPause()to conserve resources - Error Handling: CompletionListener displays error dialogs if capture fails
-
Resolution Control: Sets camera resolution using
Step 7: Creating Custom Drawable Resources for Resolution Selector
Create custom drawable files for the modern resolution selector UI:
-
Create
radio_selector.xmlinres/drawable/:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_checked="true"> <shape android:shape="rectangle"> <solid android:color="#1976D2"/> <corners android:radius="8dp"/> </shape> </item> <item android:state_checked="false"> <shape android:shape="rectangle"> <solid android:color="#FFFFFF"/> <corners android:radius="8dp"/> </shape> </item> </selector> -
Create
circle_background_blue.xmlinres/drawable/:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#1976D2"/> </shape> -
Create
circle_background_green.xmlinres/drawable/:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#4CAF50"/> </shape> -
Create
radio_text_color.xmlinres/color/:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_checked="true" android:color="#FFFFFF"/> <item android:state_checked="false" android:color="#757575"/> </selector>
Step 8: Creating the Results Fragment to Display Driver's License Information
-
Create a layout file named
fragment_result.xmlcontaining aListViewto display the driver's license information:
<?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=".fragments.ResultFragment"> <ListView android:id="@+id/lv_result" android:padding="10dp" android:layout_width="match_parent" android:layout_height="match_parent"/> </androidx.constraintlayout.widget.ConstraintLayout> -
Inflate the layout and load data from
viewModelin theResultFragment.javafile:
package com.dynamsoft.dcv.driverslicensescanner.fragments; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.dynamsoft.dcv.driverslicensescanner.MainViewModel; import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentResultBinding; public class ResultFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentResultBinding binding = FragmentResultBinding.inflate(inflater, container, false); MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, viewModel.results); binding.lvResult.setAdapter(adapter); return binding.getRoot(); } }
Driver's License Recognition with Google ML Kit
Google's ML Kit provides barcode scanning capabilities that can detect PDF417 barcodes commonly used on US driver's licenses. The Barcode.DriverLicense class provides predefined fields for extracting driver's license information.
Our implementation integrates ML Kit with a custom camera UI to provide resolution control and real-time scanning with visual feedback.
Complete Google ML Kit Implementation with Resolution Control
For a production-ready implementation that matches our Dynamsoft scanner with resolution selection, follow these steps:
Step 1: Add Google ML Kit Dependencies
In your module's build.gradle:
implementation 'com.google.mlkit:barcode-scanning:17.3.0'
Step 2: Create Google Scanner Fragment Layout
Create fragment_google_scanner.xml:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.dynamsoft.dcv.driverslicensescanner.google.ui.camera.CameraSourcePreview
android:id="@+id/preview"
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">
<com.dynamsoft.dcv.driverslicensescanner.google.ui.camera.GraphicOverlay
android:id="@+id/graphicOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.dynamsoft.dcv.driverslicensescanner.google.ui.camera.CameraSourcePreview>
<TextView
android:id="@+id/tv_resolution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="@android:color/white"
android:textSize="16sp"
android:elevation="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Step 3: Implement GoogleScannerFragment with Resolution Support
Note: The actual implementation in the repository uses ML Kit's BarcodeScanning API with a custom camera UI based on Google Vision samples. The code below shows the core concepts. For the complete implementation including CameraSourcePreview, FrameProcessor, and GraphicOverlay, see the full source code.
Create GoogleScannerFragment.java:
package com.dynamsoft.dcv.driverslicensescanner.google;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
import com.dynamsoft.dcv.driverslicensescanner.R;
import com.google.android.gms.vision.CameraSource;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;
import java.io.IOException;
public class GoogleScannerFragment extends Fragment implements CameraSourcePreview.OnCameraStartedListener {
private CameraSource cameraSource;
private CameraSourcePreview cameraSourcePreview;
private MainViewModel viewModel;
private TextView tvResolution;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_google_scanner, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
viewModel.reset();
cameraSourcePreview = view.findViewById(R.id.preview);
tvResolution = view.findViewById(R.id.tv_resolution);
cameraSourcePreview.setOnCameraStartedListener(this);
createCameraSource();
startCameraSource();
}
private void createCameraSource() {
BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(requireContext())
.setBarcodeFormats(Barcode.PDF417)
.build();
barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
@Override
public void receiveDetections(Detector.Detections<Barcode> detections) {
SparseArray<Barcode> barcodes = detections.getDetectedItems();
if (barcodes.size() > 0) {
Barcode barcode = barcodes.valueAt(0);
if (barcode.format == Barcode.PDF417) {
Barcode.DriverLicense driverLicense = barcode.driverLicense;
if (driverLicense != null) {
viewModel.parsedText = formatDriverLicenseData(driverLicense);
requireActivity().runOnUiThread(() -> {
NavHostFragment.findNavController(GoogleScannerFragment.this)
.navigate(R.id.action_GoogleScannerFragment_to_ResultFragment);
});
}
}
}
}
@Override
public void release() {}
});
// Set camera resolution based on user selection (0: 720P, 1: 1080P)
int width, height;
if (viewModel.resolutionIndex == 0) {
width = 1280;
height = 720;
} else {
width = 1920;
height = 1080;
}
cameraSource = new CameraSource.Builder(requireContext(), barcodeDetector)
.setFacing(CameraSource.CAMERA_FACING_BACK)
.setRequestedPreviewSize(width, height)
.setAutoFocusEnabled(true)
.build();
}
private void startCameraSource() {
if (cameraSource != null) {
try {
if (ActivityCompat.checkSelfPermission(requireContext(),
Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
cameraSourcePreview.start(cameraSource);
} catch (IOException e) {
e.printStackTrace();
cameraSource.release();
cameraSource = null;
}
}
}
@Override
public void onCameraStarted(int width, int height, int orientation) {
// Handle orientation for consistent display
int displayWidth = width;
int displayHeight = height;
if (orientation == 90 || orientation == 270) {
// Swap width and height for portrait orientation
displayWidth = height;
displayHeight = width;
}
final String resolutionText = displayWidth + "x" + displayHeight;
requireActivity().runOnUiThread(() -> {
tvResolution.setText(resolutionText);
tvResolution.setVisibility(View.VISIBLE);
});
}
private String formatDriverLicenseData(Barcode.DriverLicense dl) {
StringBuilder sb = new StringBuilder();
sb.append("Document Type: ").append(dl.documentType).append("\n");
sb.append("First Name: ").append(dl.firstName).append("\n");
sb.append("Middle Name: ").append(dl.middleName).append("\n");
sb.append("Last Name: ").append(dl.lastName).append("\n");
sb.append("Gender: ").append(dl.gender).append("\n");
sb.append("Street: ").append(dl.addressStreet).append("\n");
sb.append("City: ").append(dl.addressCity).append("\n");
sb.append("State: ").append(dl.addressState).append("\n");
sb.append("Zip: ").append(dl.addressZip).append("\n");
sb.append("License Number: ").append(dl.licenseNumber).append("\n");
sb.append("Issue Date: ").append(dl.issueDate).append("\n");
sb.append("Expiry Date: ").append(dl.expiryDate).append("\n");
sb.append("Birth Date: ").append(dl.birthDate).append("\n");
sb.append("Issuing Country: ").append(dl.issuingCountry).append("\n");
return sb.toString();
}
@Override
public void onResume() {
super.onResume();
startCameraSource();
}
@Override
public void onPause() {
super.onPause();
if (cameraSourcePreview != null) {
cameraSourcePreview.stop();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (cameraSource != null) {
cameraSource.release();
}
}
}
Step 4: Implement Custom CameraSourcePreview
Create CameraSourcePreview.java to handle camera lifecycle:
package com.dynamsoft.dcv.driverslicensescanner.google;
import android.content.Context;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import com.google.android.gms.vision.CameraSource;
import java.io.IOException;
public class CameraSourcePreview extends ViewGroup {
private SurfaceView surfaceView;
private boolean startRequested = false;
private boolean surfaceAvailable = false;
private CameraSource cameraSource;
private OnCameraStartedListener mListener;
public interface OnCameraStartedListener {
void onCameraStarted(int width, int height, int orientation);
}
public CameraSourcePreview(Context context, AttributeSet attrs) {
super(context, attrs);
surfaceView = new SurfaceView(context);
surfaceView.getHolder().addCallback(new SurfaceCallback());
addView(surfaceView);
}
public void setOnCameraStartedListener(OnCameraStartedListener listener) {
mListener = listener;
}
public void start(CameraSource cameraSource) throws IOException {
if (cameraSource == null) {
stop();
}
this.cameraSource = cameraSource;
if (this.cameraSource != null) {
startRequested = true;
startIfReady();
}
}
public void stop() {
if (cameraSource != null) {
cameraSource.stop();
}
}
private void startIfReady() throws IOException {
if (startRequested && surfaceAvailable) {
cameraSource.start(surfaceView.getHolder());
if (mListener != null) {
mListener.onCameraStarted(
cameraSource.getPreviewSize().getWidth(),
cameraSource.getPreviewSize().getHeight(),
0
);
}
requestLayout();
startRequested = false;
}
}
private class SurfaceCallback implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder surface) {
surfaceAvailable = true;
try {
startIfReady();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder surface) {
surfaceAvailable = false;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = right - left;
int height = bottom - top;
if (cameraSource != null) {
android.util.Size size = cameraSource.getPreviewSize();
if (size != null) {
float ratio = (float) size.getWidth() / size.getHeight();
int layoutWidth = width;
int layoutHeight = (int) (width / ratio);
if (layoutHeight < height) {
layoutHeight = height;
layoutWidth = (int) (height * ratio);
}
for (int i = 0; i < getChildCount(); ++i) {
getChildAt(i).layout(0, 0, layoutWidth, layoutHeight);
}
}
}
}
}
Key Features:
- Resolution Control: Dynamically sets camera preview size based on user selection (720P or 1080P)
- OnCameraStartedListener: Callback interface to display actual camera resolution
- Orientation Handling: Swaps width/height for portrait mode to ensure consistent resolution display
- PDF417 Detection: Specifically configured for driver's license barcodes
- Auto-parsing: Automatically extracts all AAMVA driver's license fields




Top comments (0)