五分鐘學會 Android Studio QR code 掃描器 一步步製作教學(附上完整程式碼)

今天查看電子信箱時發現有位網友 Jimmy 傳了一封 Email,詢問關於筆者於 2018 年發表的文章:5分鐘教會 Android Studio QR code 掃描器 製作 教學中的問題。 在多年後的今天審視自己幾年前發表的文章,發現有些過時,也有許多小細節沒有寫到,筆者決定將這篇文章重新寫過,將會詳細介紹和解答網友的問題。若正在閱讀的各位有任何不了解或是發現文章中的不足之處,還請在下方留言或是寄送 Email 給予意見回饋。

正文:

取得相機權限

要製作使用相機掃描 QR code 的手機軟體,需要能夠使用相機權限,我們需要先在 AndroidManifest.xml 內註冊相機權限:
  
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.CAMERA" />

    <application
        android:allowBackup="true">
        <!-- 省略目前不需要注意的程式碼 -->
    </application>

</manifest>


回到 MainActivity.java 中, Android 6 以後的版本,為了增加手機的安全性,除了宣告要使用權限外,任何取得或是讀寫的動作都需要使用者授權,告訴系統我們需要權限,系統會彈出視窗詢問使用者是否要給予我們這個 APP 權限,使用者選擇後系統告訴我們使用者的決定。

完整程式碼:
    
package app.ruyut.example.qrcode;

import android.Manifest;
import android.app.AlertDialog;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 取得相機權限
        getPermissionCamera();
    }

    /**
     * 自訂相機權限代號,用於判斷是否取得權限
     */
    private static final int REQUEST_CAMERA_PERMISSION = 1;

    /**
     * 取得相機權限
     */
    public void getPermissionCamera() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            // 已有相機權限,不須再詢問
            return;
        }

        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {

            // 曾經被使用者拒絕授予權限過,可以在這邊提醒使用者為何需要權限
            new AlertDialog.Builder(this)
                    .setCancelable(false)
                    .setTitle("需要相機權限")
                    .setMessage("需要相機權限才能掃描 QR Code,請授予相機權限")
                    .setPositiveButton("OK", (dialog, which) -> {
                                // 再次顯示權限授予視窗
                                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
                            }
                    )
                    .show();
        } else {
            // 第一次詢問權限,或者使用者點選「不再詢問」
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
        }
    }

    /**
     * 取得詢問相機權限的結果
     */
    @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) {
                    // 使用者同意授予權限
                    Toast.makeText(this, "已取得相機權限", Toast.LENGTH_SHORT).show();
                } else {
                    // 使用者拒絕授予權限
                    Toast.makeText(this, "未取得相機權限", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }
}
    

定義介面

在 activity_main.xml 中增加一個 TextView ,用以顯示掃描到的文字
再增加一個 widgets 頁籤內的 SurfaceView,用來顯示相機畫面

附上 activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dp"
        android:gravity="center"
        android:text="TextView"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="10dp"
        app:layout_constraintBottom_toTopOf="@+id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
    

引用掃描 QR Code 的套件

我們使用 Google Mobile Vision 套件,在 build.gradle(Module) 中引用此套件:

dependencies {

    implementation 'com.google.android.gms:play-services-vision:20.1.3'

}
    

撰寫讀取 QR Code 相關程式

宣告會使用到的元件:
    
    private SurfaceView surfaceView;
    private TextView textView;
    private CameraSource cameraSource;
    

需要 import 的 package :
    
import android.view.SurfaceView;
import android.widget.TextView;
import com.google.android.gms.vision.CameraSource;
    

綁定 activity_main.xml 的原件
    
        surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
        textView = (TextView) findViewById(R.id.textView);
    

設定套件中的條碼樣式和設定相機內容來源接收器:
    
        BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(this)
                .setBarcodeFormats(Barcode.QR_CODE).build();

        cameraSource = new CameraSource.Builder(this, barcodeDetector)
                //.setRequestedPreviewSize(300, 300) // 可以自訂預覽視窗畫面內容大小
                .setAutoFocusEnabled(true) // 自動對焦
                .build();
    

需要 import 的 package :
    
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;
    

替 SurfaceView 增加內容
    
        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
        });
    

此時會拋出錯誤,提示有需要實作的方法,使用滑鼠左鍵點擊紅色底線的文字,按下 Alt + Enter,點選 Implement methods

預設已經選擇全部方法,點選 OK

會發現裡面已經自動產生了三個方法

在 surfaceDestroyed 中輸入關閉相機的程式碼
    
cameraSource.stop();
    

在 surfaceCreated 中增加讀取相機內容的程式碼
    
                if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.CAMERA)
                        != PackageManager.PERMISSION_GRANTED)
                    return;
                try {
                    cameraSource.start(surfaceHolder);
                } catch (IOException e) {
                    e.printStackTrace();
                }
    

此步驟需要 import 的 package :
    
import android.view.SurfaceHolder;
import java.io.IOException;
    

讀取到條碼時執行的內容

貼上下列程式碼,和上面一樣使用滑鼠左鍵點擊紅色底線的文字,按下 Alt + Enter,點選 Implement methods
    
        barcodeDetector.setProcessor(new Detector.Processor<Barcode>(){

        });
    


自動產生產生方法後:
    
        barcodeDetector.setProcessor(new Detector.Processor<Barcode>(){

            @Override
            public void release() {

            }

            @Override
            public void receiveDetections(@NonNull Detector.Detections<Barcode> detections) {

            }
        });
    

在 receiveDetections 方法中貼上下列程式碼,如果掃描到資料時就會自動顯示在 textView 上
    
                final SparseArray<Barcode> qrCodes=detections.getDetectedItems();
                if(qrCodes.size()!=0){
                    textView.post(() -> textView.setText(qrCodes.valueAt(0).displayValue));
                }
    

成功!

不過目前有個小問題,就是一開始相機畫面是黑的,需要關閉 APP 重新啟動後才會有畫面,如果去設定移除權限後再重新給予權限時會發生同樣的情況,還有移除 APP 重新安裝後一開始也會有這種形況。正確來說就是如果是 APP 開啟後才給予權限,就會發生沒有相機畫面。

會發生這樣的問題是因為一開始執行時判斷沒有權限就不會顯示畫面,而後續有相機權限後它並不知道,也沒有再重新嘗試取得畫面。解決的方式也很簡單,我們可以接收授予權限後的回應事件,判斷是不是有取得相機權限,有的話就取得相機畫面。

使用 Ctrl + O 開啟 Override 視窗,找到 onRequestPermissionsResult 點選並按下 OK

輸入下面的程式碼:
    
    /**
     * 取得詢問相機權限的結果
     */
    @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) {
                    // 使用者同意授予權限
                    Toast.makeText(this, "已取得相機權限", Toast.LENGTH_SHORT).show();

                    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                        return;
                    }
                    try {
                        cameraSource.start(surfaceView.getHolder());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }


                } else {
                    // 使用者拒絕授予權限
                    Toast.makeText(this, "未取得相機權限", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }
    

這樣給予權限後就會自動重新啟動相機了!

在 Github 上附上的完整程式碼,方便讀者尋找錯誤,如果覺得有幫助到您的話希望可以給我的 Github 專案一顆星星(Star),感謝您的支持!

留言