QR Code Scanner –适用于Android的条形码扫描仪

时间:2020-02-23 14:43:43  来源:igfitidea点击:

许多应用程序中均存在用于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码检测到的相关电子邮件地址。