之前在『React Native Swiper』一文中整理了我在 React Native 中使用的第三方组件 Swiper,而当时项目中还需要集成扫描二维码的功能,于是我就查找了一些扫描二维码的第三方组件,但是装上之后却发现没有一个能够顺利调用,无奈之下就只能选择在原生上实现这个功能。

由于这个 Demo 目前只有 Android 版本,所以也只是集成了在 Android 环境下的扫码功能。

而在 Android 版本中,我使用的是『ZXing』这个第三方库,包括在后来上线的公司项目中,我使用的也是这个库,今天就来整理一下『ZXing』的使用笔记。

ZXing

ZXing』我第一次听说的时候还以为是一个中国人开发的项目,后来才知道它是「Zebra Crossing」的简写,它是一个用 Java 实现的开源的多格式 1D/2D 条形码图像处理库。

支持的格式如下:

1D product 1D industrial 2D
UPC-A Code 39 QR Code
UPC-E Code 93 Data Matrix
EAN-8 Code 128 Aztec (beta)
EAN-13 Codabar PDF 417 (beta)
ITF MaxiCode
RSS-14
RSS-Expanded

虽然很多格式我都没听过,但我只需要知道它支持 QR Code 就可以了,毕竟 QR Code 在国内的应用是发挥到了极致。

由于『ZXing』的可定制性高,所以也衍生出了许多基于『ZXing』的第三方开源项目,而且在国内的技术论坛也有很多民间的定制版本。

话不多说,马上来看看『ZXing』的使用方法。

首先得添加依赖:

dependencies {
    implementation 'com.google.zxing:core:(insert latest version)'
}

其实现在已经有大量的开发者在『ZXing』上进行了集成、优化或者精简,使得我们这些小白用户能够在未搞懂原理的情况下轻易的接入到项目中。

因为『ZXing』的项目非常庞大,而我在项目中仅需用到扫描二维码的功能,其他许多功能我并不需要,所以我还是使用精简过的『ZXing』比较合适。

找了一圈,发现国内开发者 ahuyangdong 修改过并做的 Demo 简单易用,比较适合我,于是就采用了他的方案,项目『QrCodeDemo4』。

另外,国内开发者 HappyMiao 的『QrCodeScan』也有相似性。

QrCodeDemo4』有持续更新,而我使用的是较老一些的版本。

先把『QrCodeDemo4』中 com.google.zxing 整个包 Copy 到自己的项目,包内共有 5 个目录:

复制的目录

再把『QrCodeDemo4』的布局文件「activity_scanner.xml」和「toolbar_scanner.xml」拉到自己的项目中,这里实现的是扫描二维码的页面,我个人觉得没有必要分开,于是就整合到了一起:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">

    <ImageView
        android:id="@+id/drop_scanner"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimaryDark"
        android:src="@drawable/ic_drop" />

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <SurfaceView
            android:id="@+id/scanner_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center" />

        <com.google.zxing.view.ViewfinderView
            android:id="@+id/viewfinder_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:corner_color="@color/corner_color"
            app:frame_color="@color/viewfinder_frame"
            app:label_text="扫描二维码"
            app:label_text_color="@color/colorAccent"
            app:laser_color="@color/laser_color"
            app:mask_color="@color/viewfinder_mask"
            app:result_color="@color/result_view"
            app:result_point_color="@color/result_point_color"/>

    </FrameLayout>

</LinearLayout>

这里我做了少许修改,因为我已经在超过 3 个项目中集成过『ZXing』,所以在不同项目中我的修改方式都不尽相同,比如在这里我直接使用了 ImageView 来代替 Toolbar,而在有的项目中我则是直接使用 Toolbar

然后把「raw」目录下的「beep.ogg」也复制到项目:

扫描音频文件

这个其实是音频文件,是扫描成功时的提示音。

接着再把「values」目录下的「attrs.xml」、「ids.xml」和「colors.xml」这三个文件进行合并或拷贝,即没有该文件则直接 Copy 到项目,有则跟原文件进行合并。

配置资源文件

我这里「attrs.xml」和「ids.xml」都没有,所以直接复制,「colors.xml」默认就有了,则在后面追加相应的内容:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#d9f3ff</color>
    <color name="colorAccent">#00A0E9</color>
    <color name="waveBackColor">#66C7F3</color>

    <!-- QRCode scanner color begin -->
    <color name="viewfinder_mask">#B0000000</color>
    <color name="result_view">#B0000000</color>
    <color name="viewfinder_frame">#90FFFFFF</color>
    <color name="result_point_color">#C0FFFF00</color>
    <color name="laser_color">#00A0E9</color>
    <color name="corner_color">#00A0E9</color>
    <!-- QRCode scanner color end -->
</resources>

其实对应的目录也是可以从『ZXing』官方开源项目中找到的,根据个人喜好下载即可。

现在打开 com.google.zxing 包内的文件如无意外应该是会有报错的,应该是 R 文件引用有误,修改为本项目的 R 即可,相应文件参考如下:

com.google.zxing.activity.CaptureActivity
com.google.zxing.decoding.CaptureActivityHandler
com.google.zxing.decoding.DecodeHandler
com.google.zxing.view.ViewfinderView

集成部分终于完成了,接下来就该编码实现功能了。

在「AndroidManifest.xml」中添加相应的权限:

<uses-permission android:name="android.permission.INTERNET" />      <!-- 网络权限 -->
<uses-permission android:name="android.permission.VIBRATE" />       <!-- 震动权限 -->
<uses-permission android:name="android.permission.CAMERA" />        <!-- 摄像头权限 -->
<uses-feature android:name="android.hardware.camera.autofocus" />   <!-- 自动聚焦权 -->

并注册 CaptureActivity,该 Activity 就是扫描二维码页面。

<application ... >
    ...
    <activity android:name="com.google.zxing.activity.CaptureActivity" />
</application>

需要注意的是,高版本的 Android 中需要动态申请 Camera 权限,调用源码如下:

public class MainActivity extends AppCompatActivity {

    final static int REQ_PERM_CAMERA = 1;
    final static int REQ_QR_CODE = 2;

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

        Button scanButton = (Button) findViewById(R.id.scan_btn);
        scanButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startScanQrCode();
            }
        });
    }

    /**
     * 开始扫描二维码
     */
    void startScanQrCode() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            // 申请权限
            ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manifest.permission.CAMERA}, REQ_PERM_CAMERA);
            return;
        }
        // 二维码扫描
        Intent intent = new Intent(MainActivity.this, CaptureActivity.class);
        startActivityForResult(intent, REQ_QR_CODE);
        overridePendingTransition(R.anim.activity_open, R.anim.activity_stay);
    }

    /**
     * 动态申请权限
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case REQ_PERM_CAMERA:
                // 摄像头权限申请
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    startScanQrCode();
                } else {        // 授权被禁止
                    Toast.makeText(MainActivity.this, "请至权限中心打开本应用的相机访问权限", Toast.LENGTH_LONG).show();
                }
                break;

            default:
        }
    }

    /**
     * 扫码结果回调
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // 扫描结果回调
        if (requestCode == REQ_QR_CODE && resultCode == 161) {
            Bundle bundle = data.getExtras();
            String scanResult = bundle.getString(INTENT_EXTRA_KEY_QR_SCAN);
            // scanResult 就是扫描二维码得到的字符串,再次可以根据自己的逻辑编写代码
        }
    }
}

这里有个小问题,在回调的这段代码中:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    // 扫描结果回调
    if (requestCode == REQ_QR_CODE && resultCode == 161) {
        ...
    }
}

很多技术博客中使用的是 resultCode==RESULT_OK,其中 RESULT_OK 的值是 -1,但是我发现在我第一个集成的项目(就是用 React Native 写的 Demo)中出现了问题,修改后发现修改为 resultCode==161 后行得通,我也不知道为什么,所以在后期我所有的项目中我都将其写为 161,不知道是因为我项目的特殊性还是什么原因,并没有深究。

放一下当初 React Native 写的 Demo 集成『ZXing』后运行扫描二维码的功能吧:

效果

我尝试去看『ZXing』的源码,探索一下原理,但发现太高深了,很难看懂,还是等哪天技术提升了再来研究吧。

大概说一下几个主要类的功能:

CaptureActivity 是扫描界面,也是官方 Demo 的主界面。

CaptureActivityHandler 是辅助扫描界面,进行一些逻辑的处理和消息的转发。

CameraManagerCamera 是相机有关的部分,如预览和自动聚焦等。

DecodeThreadDecodeHandler 是跟解码有关的类,包括线程和消息处理。

BarcodeFormatDecodeHintType 内有支持的一些类型、格式和配置等。如二维码、各种条形码和字符集。

其他就不一一说明了,看一下扫码流程:

ZXing 二维码扫描过程