Сканер QR-кода - сканер штрих-кода для Android
Сканер QR-кода или сканер штрих-кода для функций Android присутствуют во многих приложениях для чтения некоторых полезных данных. В этом руководстве мы обсудим и реализуем API штрих-кода, присутствующий в API Google Mobile Vision. Чтобы узнать о реализации распознавания лиц с помощью Vision API, обратитесь сюда.
Сканер штрих-кода для Android
- Штрих-коды 1D: EAN-13, EAN-8, UPC-A, UPC-E, Code-39, Code-93, Code-128, ITF, Codabar
- Двумерные штрих-коды: QR-код, матрица данных, PDF-417, AZTEC.
Штрих-коды могут сканировать от URL-адреса, контактной информации до геолокации, WIFI, идентификаторов водительских прав. QR-код является более популярным форматом и обычно используется во многих приложениях. Ниже мы разработаем приложение, которое сканирует значение QR-кода из растрового изображения, а также обнаруживает QR-код с помощью камеры и выполняет соответствующие действия.
Структура проекта QR Code Scanner для Android
Настройка Android Studio для библиотеки штрих-кодов
Добавьте следующее в файл build.gradle
.
implementation 'com.google.android.gms:play-services-vision:11.8.0'
Добавьте следующее в тег приложения файла AndroidManifest.xml
, чтобы включить обнаружение штрих-кода в вашем приложении.
<meta-data
android:name="com.google.android.gms.vision.DEPENDENCIES"
android:value="barcode" />
Сканер QR-кода с изображения
Код файла макета activity_main.xml
приведен ниже.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Button
android:id="@+id/btnTakePicture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/take_barcode_picture" />
<Button
android:id="@+id/btnScanBarcode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btnTakePicture"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:text="@string/scan_barcode" />
</RelativeLayout>
Код для MainActivity.java приведен ниже.
package com.journaldev.barcodevisionapi;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
Button btnTakePicture, btnScanBarcode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews() {
btnTakePicture = findViewById(R.id.btnTakePicture);
btnScanBarcode = findViewById(R.id.btnScanBarcode);
btnTakePicture.setOnClickListener(this);
btnScanBarcode.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnTakePicture:
startActivity(new Intent(MainActivity.this, PictureBarcodeActivity.class));
break;
case R.id.btnScanBarcode:
startActivity(new Intent(MainActivity.this, ScannedBarcodeActivity.class));
break;
}
}
}
MainActivity.java
содержит две кнопки. Первый запускает действие, которое сканирует QR-код в растровом изображении, полученном с камеры, и возвращает данные, представленные в QR-коде (если есть). Второй сканирует QRCode и обнаруживает их в режиме реального времени. Прежде чем мы перейдем к бизнес-логике приложения, нам нужно добавить следующие разрешения в файл AndroidManifest.xml
.
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
Чтобы предоставить доступ к файлам, созданным другими приложениями, нам нужно добавить следующий тег provider
внутрь нашего тега приложения в файле AndroidManifest.xml
.
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
Это необходимо для получения изображения, снятого камерой, в нашем сканере QR-кода для приложения Android. Начнем с первого, а именно с PictureBarcodeActivity.java
. Код для XML-макета activity_barcode_picture.xml
приведен ниже.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageView"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_centerHorizontal="true"
android:src="@mipmap/journaldev_logo" />
<TextView
android:id="@+id/txtResultsHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/imageView"
android:layout_centerHorizontal="true"
android:text="Results"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtResultsBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/txtResultsHeader"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:gravity="center" />
<Button
android:id="@+id/btnOpenCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:text="@string/open_camera" />
</RelativeLayout>
Код класса PictureCodeActivity.java
приведен ниже.
package com.journaldev.barcodevisionapi;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.vision.Frame;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;
import java.io.File;
import java.io.FileNotFoundException;
public class PictureBarcodeActivity extends AppCompatActivity implements View.OnClickListener {
Button btnOpenCamera;
TextView txtResultBody;
private BarcodeDetector detector;
private Uri imageUri;
private static final int REQUEST_CAMERA_PERMISSION = 200;
private static final int CAMERA_REQUEST = 101;
private static final String TAG = "API123";
private static final String SAVED_INSTANCE_URI = "uri";
private static final String SAVED_INSTANCE_RESULT = "result";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_barcode_picture);
initViews();
if (savedInstanceState != null) {
if (imageUri != null) {
imageUri = Uri.parse(savedInstanceState.getString(SAVED_INSTANCE_URI));
txtResultBody.setText(savedInstanceState.getString(SAVED_INSTANCE_RESULT));
}
}
detector = new BarcodeDetector.Builder(getApplicationContext())
.setBarcodeFormats(Barcode.DATA_MATRIX | Barcode.QR_CODE)
.build();
if (!detector.isOperational()) {
txtResultBody.setText("Detector initialisation failed");
return;
}
}
private void initViews() {
txtResultBody = findViewById(R.id.txtResultsBody);
btnOpenCamera = findViewById(R.id.btnTakePicture);
txtResultBody = findViewById(R.id.txtResultsBody);
btnOpenCamera.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnTakePicture:
ActivityCompat.requestPermissions(PictureBarcodeActivity.this, new
String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_CAMERA_PERMISSION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
takeBarcodePicture();
} else {
Toast.makeText(getApplicationContext(), "Permission Denied!", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST && resultCode == RESULT_OK) {
launchMediaScanIntent();
try {
Bitmap bitmap = decodeBitmapUri(this, imageUri);
if (detector.isOperational() && bitmap != null) {
Frame frame = new Frame.Builder().setBitmap(bitmap).build();
SparseArray<Barcode> barcodes = detector.detect(frame);
for (int index = 0; index < barcodes.size(); index++) {
Barcode code = barcodes.valueAt(index);
txtResultBody.setText(txtResultBody.getText() + "\n" + code.displayValue + "\n");
int type = barcodes.valueAt(index).valueFormat;
switch (type) {
case Barcode.CONTACT_INFO:
Log.i(TAG, code.contactInfo.title);
break;
case Barcode.EMAIL:
Log.i(TAG, code.displayValue);
break;
case Barcode.ISBN:
Log.i(TAG, code.rawValue);
break;
case Barcode.PHONE:
Log.i(TAG, code.phone.number);
break;
case Barcode.PRODUCT:
Log.i(TAG, code.rawValue);
break;
case Barcode.SMS:
Log.i(TAG, code.sms.message);
break;
case Barcode.TEXT:
Log.i(TAG, code.displayValue);
break;
case Barcode.URL:
Log.i(TAG, "url: " + code.displayValue);
break;
case Barcode.WIFI:
Log.i(TAG, code.wifi.ssid);
break;
case Barcode.GEO:
Log.i(TAG, code.geoPoint.lat + ":" + code.geoPoint.lng);
break;
case Barcode.CALENDAR_EVENT:
Log.i(TAG, code.calendarEvent.description);
break;
case Barcode.DRIVER_LICENSE:
Log.i(TAG, code.driverLicense.licenseNumber);
break;
default:
Log.i(TAG, code.rawValue);
break;
}
}
if (barcodes.size() == 0) {
txtResultBody.setText("No barcode could be detected. Please try again.");
}
} else {
txtResultBody.setText("Detector initialisation failed");
}
} catch (Exception e) {
Toast.makeText(getApplicationContext(), "Failed to load Image", Toast.LENGTH_SHORT)
.show();
Log.e(TAG, e.toString());
}
}
}
private void takeBarcodePicture() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File photo = new File(Environment.getExternalStorageDirectory(), "pic.jpg");
imageUri = FileProvider.getUriForFile(PictureBarcodeActivity.this,
BuildConfig.APPLICATION_ID + ".provider", photo);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, CAMERA_REQUEST);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (imageUri != null) {
outState.putString(SAVED_INSTANCE_URI, imageUri.toString());
outState.putString(SAVED_INSTANCE_RESULT, txtResultBody.getText().toString());
}
super.onSaveInstanceState(outState);
}
private void launchMediaScanIntent() {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(imageUri);
this.sendBroadcast(mediaScanIntent);
}
private Bitmap decodeBitmapUri(Context ctx, Uri uri) throws FileNotFoundException {
int targetW = 600;
int targetH = 600;
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(ctx.getContentResolver().openInputStream(uri), null, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
return BitmapFactory.decodeStream(ctx.getContentResolver()
.openInputStream(uri), null, bmOptions);
}
}
Несколько выводов, сделанных из приведенного выше кода, приведены ниже.
-
The following code creates a Barcode Detector.
detector = new BarcodeDetector.Builder(getApplicationContext()) .setBarcodeFormats(Barcode.DATA_MATRIX | Barcode.QR_CODE) .build();
-
The types of formats to be scanned are set inside the method
setBarcodeFormats()
. -
takeBarcodePicture()
function is where the camera is launched. To retrieve the image we use thelaunchMediaScanIntent()
that calls a broadcast Intent to search for the image using the image URI. -
A
Frame.Builder
is used to create a frame of the Bitmap image. Over the frame, the Barcode detector scans for the possible QR Codes. The following line in the above code creates a Frame out of the Bitmap.Frame frame = new Frame.Builder().setBitmap(bitmap).build();
-
We’ve created a SparseArray that’ll contain all possible QR Codes present in the image by invoking the
detect()
method over the Barcode Detector.SparseArray<Barcode> barcodes = detector.detect(frame);
To get the format of the QR Code, the
valueFormat
field is called over the Barcode instance as shown below.barcodes.valueAt(index).valueFormat
-
To get the displayed value and the raw values, the following are invoked.
barcodes.valueAt(index).displayValue barcodes.valueAt(index).rawValue
-
The relevant value returned is displayed in the TextView. For then one Barcodes in a Bitmap, their values are appended to the current TextView.
Класс ScannedBarcodeActivity.java
сканирует штрих-код с помощью камеры. Отсюда мы сгенерировали два собственных пользовательских QR-кода. Код для макета activity_scan_barcode.xml
приведен ниже.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_horizontal_margin">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/btnAction"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true" />
<TextView
android:id="@+id/txtBarcodeValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:text="No Barcode Detected"
android:textColor="@android:color/white"
android:textSize="20sp" />
<Button
android:id="@+id/btnAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="ADD CONTENT IN THE MAIL" />
</RelativeLayout>
Сканер штрих-кода для Android с камеры
Код для ScannedBarcodeActivity.java
приведен ниже.
package com.journaldev.barcodevisionapi;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.SparseArray;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
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 ScannedBarcodeActivity extends AppCompatActivity {
SurfaceView surfaceView;
TextView txtBarcodeValue;
private BarcodeDetector barcodeDetector;
private CameraSource cameraSource;
private static final int REQUEST_CAMERA_PERMISSION = 201;
Button btnAction;
String intentData = "";
boolean isEmail = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scan_barcode);
initViews();
}
private void initViews() {
txtBarcodeValue = findViewById(R.id.txtBarcodeValue);
surfaceView = findViewById(R.id.surfaceView);
btnAction = findViewById(R.id.btnAction);
btnAction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (intentData.length() > 0) {
if (isEmail)
startActivity(new Intent(ScannedBarcodeActivity.this, EmailActivity.class).putExtra("email_address", intentData));
else {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(intentData)));
}
}
}
});
}
private void initialiseDetectorsAndSources() {
Toast.makeText(getApplicationContext(), "Barcode scanner started", Toast.LENGTH_SHORT).show();
barcodeDetector = new BarcodeDetector.Builder(this)
.setBarcodeFormats(Barcode.ALL_FORMATS)
.build();
cameraSource = new CameraSource.Builder(this, barcodeDetector)
.setRequestedPreviewSize(1920, 1080)
.setAutoFocusEnabled(true) //you should add this feature
.build();
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
if (ActivityCompat.checkSelfPermission(ScannedBarcodeActivity.this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
cameraSource.start(surfaceView.getHolder());
} else {
ActivityCompat.requestPermissions(ScannedBarcodeActivity.this, new
String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
cameraSource.stop();
}
});
barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
@Override
public void release() {
Toast.makeText(getApplicationContext(), "To prevent memory leaks barcode scanner has been stopped", Toast.LENGTH_SHORT).show();
}
@Override
public void receiveDetections(Detector.Detections<Barcode> detections) {
final SparseArray<Barcode> barcodes = detections.getDetectedItems();
if (barcodes.size() != 0) {
txtBarcodeValue.post(new Runnable() {
@Override
public void run() {
if (barcodes.valueAt(0).email != null) {
txtBarcodeValue.removeCallbacks(null);
intentData = barcodes.valueAt(0).email.address;
txtBarcodeValue.setText(intentData);
isEmail = true;
btnAction.setText("ADD CONTENT TO THE MAIL");
} else {
isEmail = false;
btnAction.setText("LAUNCH URL");
intentData = barcodes.valueAt(0).displayValue;
txtBarcodeValue.setText(intentData);
}
}
});
}
}
});
}
@Override
protected void onPause() {
super.onPause();
cameraSource.release();
}
@Override
protected void onResume() {
super.onResume();
initialiseDetectorsAndSources();
}
}
Несколько выводов, сделанных из приведенного выше кода, приведены ниже.
-
SurfaceView
удобен для отображения изображений предварительного просмотра с камеры, поскольку он быстро отображает графический интерфейс. ИнтерфейсSurfaceHolder.Callback
используется для получения информации об изменениях, происходящих на поверхности (в данном случае — превью камеры).SurfaceHolder.Callback
реализует три метода:- SurfaceChanged: этот метод вызывается при изменении размера или формата поверхности.
- SurfaceCreated : этот метод вызывается при первом создании поверхности.
- SurfaceDestroyed: вызывается при разрушении поверхности.
CameraSource
управляет камерой в сочетании с базовым детектором. Здесь SurfaceView является базовым детектором.CameraSource.start()
открывает камеру и начинает отправлять кадры предварительного просмотра в SurfaceView. CameraSource создается следующим образом:cameraSource = new CameraSource.Builder(this, barcodeDetector) .setRequestedPreviewSize(1920, 1080) .setAutoFocusEnabled(true) //you should add this feature .build();
Мы назначили процессор детектору штрих-кода с помощью
setProcessor()
. Интерфейс содержит обратный вызов метода receiveDetections(), который получает QR-код из предварительного просмотра камеры и добавляет их в SparseArray.Значение QR-кода отображается в TextView с помощью Runnable, поскольку штрих-коды обнаруживаются в фоновом потоке.
В этом примере мы создали два штрих-кода с помощью генератора. Один содержит URL. Второй содержит адрес электронной почты. При нажатии кнопки в зависимости от обнаруженного значения QR-кода мы либо запустим URL-адрес, либо отправим электронное письмо на соответствующий адрес электронной почты, обнаруженный из QR-кода.
Скачать QR-код, пример проекта сканера штрих-кода