QR Code Scanner –适用于Android的条形码扫描仪
许多应用程序中均存在用于Android功能的QR码扫描仪或者条形码扫描仪,以读取一些有用的数据。
在本教程中,我们将讨论和实现Google Mobile Vision API中存在的Barcode API。
要了解使用Vision API进行人脸检测的实现,请参阅此处。
适用于Android的条形码扫描仪
随着Google Vision API的引入,开发人员在应用程序中实现条形码变得更加容易。
以下是Vision API支持的主要格式。
- 一维条形码:EAN-13,EAN-8,UPC-A,UPC-E,Code-39,Code-93,Code-128,ITF,Codabar
- 2D条码:QR码,数据矩阵,PDF-417,AZTEC
条形码可以扫描各种内容,包括URL,联系信息,地理位置,WIFI,驾驶执照ID。
QR码是更流行的格式,在许多应用程序中都很常见。
下面,我们将开发一个应用程序,该程序从位图图像中扫描QR Code值,并通过摄像头检测QR Code并执行相关操作。
适用于Android项目结构的QR Code扫描仪
为条形码库配置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.theitroad.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"
要共享和访问其他应用程序创建的文件,我们需要在AndroidManifest.xml文件的应用程序标签内添加以下provider
标签。
<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>
这是在Android应用程序的QR码扫描仪中检索相机捕获的图像所必需的。
让我们从第一个开始,即" 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/theitroad_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.theitroad.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); } }
从上面的代码得出的推论很少,下面给出。
以下代码创建一个条形码检测器。
可以在setBarcodeFormats()方法中设置要扫描的格式类型。
takeBarcodePicture()函数是启动相机的位置。
要检索图像,我们使用launchMediaScanIntent()
,它调用广播Intent使用图像URI搜索图像。一个Frame.Builder用于创建位图图像的框架。
在框架上,条形码检测器扫描可能的QR码。
上面的代码中的以下行从位图创建帧。
- 通过在条形码检测器上调用
detect()
方法,我们创建了一个SparseArray,其中包含图像中可能存在的所有QR码。
要获取QR码的格式,请在"条形码"实例上调用" valueFormat"字段,如下所示。
要获取显示的值和原始值,将调用以下内容。
返回的相关值显示在TextView中。
对于位图中的一个条形码,其值将附加到当前的TextView中。
ScannedBarcodeActivity.java
类通过摄像机扫描条形码。
我们从这里生成了两个自己的自定义QR码。
下面给出了" activity_scan_barcode.xml"的布局代码。
detector = new BarcodeDetector.Builder(getApplicationContext()) .setBarcodeFormats(Barcode.DATA_MATRIX | Barcode.QR_CODE) .build();
相机的Android条码扫描仪
下面给出了ScannedBarcodeActivity.java的代码。
Frame frame = new Frame.Builder().setBitmap(bitmap).build();
从上面的代码得出的推论很少,下面给出。
SurfaceView
非常适合显示摄像机预览图像,因为它可以快速呈现GUI。
接口" SurfaceHolder.Callback"用于接收有关表面上发生的更改的信息(在这种情况下为相机预览)。SurfaceHolder.Callback
实现了三种方法:SurfaceChanged:当表面的大小或者格式改变时,调用此方法。
SurfaceCreated:首先创建表面时,将调用此方法。
SurfaceDestroyed:破坏表面时调用。
" CameraSource"与底层探测器一起管理摄像机。
这里的SurfaceView是基础检测器。
" CameraSource.start()"将打开相机并开始将预览帧发送到SurfaceView。
CameraSource是通过以下方式创建的:
- 我们已使用
setProcessor()
在条形码检测器上分配了处理器。
该接口包含对方法receiveDetections()的回调,该方法从摄像机预览接收QR码并将其添加到SparseArray中。
由于在后台线程中检测到条形码,因此QR代码的值将使用Runnable在TextView中显示。
在此示例中,我们使用生成器创建了两个条形码。
一个包含URL。
第二个包含一个电子邮件地址。
点击按钮后,我们将根据检测到的QR码值启动URL或者将电子邮件发送到从QR码检测到的相关电子邮件地址。