扫码服务-HMS

2,032 阅读9分钟

概述

关于APP扫码,市场上提供了多种选择,这些扫码工具在正常环境扫普通码效果和速度都还可以,当扫码环境出现强光、昏暗、反光、吸光、模糊、残缺、变形、太近或太远、倾斜有夹角等情况,扫码效果不尽如人意。开发者如果对扫码没有很深入的研究是很难做出像微信、支付宝那样的扫码,而且通常开发时间和开发规模也不允许开发者在扫码上花费太多的精力和时间,微信和支付宝也没有开放相关的扫码服务,所以能够在复杂环境下使用且免费的扫码选择并不多。华为统一扫码服务提供便捷的二维码与条形码扫描、解析、生成能力,帮助开发者快速构建应用内的扫码功能。得益于华为在计算机视觉领域能力的积累,Scan Kit可以实现远距离二维码的检测与自动放大,针对常见复杂扫码场景,如:强光照、污损、柱面等,做了针对性识别优化,提升扫码成功率与用户体验。

场景

扫码

华为扫码服务支持扫描13种全球主流的码制式。如果应用只处理部分特定的码制式,开发者可以在接口中指定制式以便加快扫码速度。已支持的码制式:

  • 一维码:EAN-8、EAN-13、UPC-A、UPC-E、Codabar、Code 39、Code 93、Code 128、ITF
  • 二维码:QR Code、Data Matrix、PDF417、Aztec

华为扫码服务提供多种调用模式,开发者可以根据需求选择合适的扫码功能:

方式平台流程界面功能
Default View ModeAndroid/iOSScan Kit处理Scan Kit提供相机扫码(可以调用Bitmap mode增加导入图片扫码功能)。
Customized View ModeAndroid/iOSScan Kit处理自定义相机扫码(可以调用Bitmap mode增加导入图片扫码功能)。
Bitmap ModeAndroid/iOS开发者应用处理自定义相机扫码、导入图片扫码。
MultiProcessor ModeAndroid开发者应用处理自定义相机扫码、导入图片扫码,支持同时检测多个码。

解码

Scan Kit可以将码的原始内容返回给开发者,还会针对使用特定内容格式编码的二维码/条形码进行分析并提取结构化数据,帮助开发者快速构建关联服务。已支持如下场景:联系人信息、Wi-Fi连接信息、网页、日历日程、ID卡、短信、电话、邮件、地理位置、商品条码、ISBN。

生码

Scan Kit支持将字符串转换为一维码或二维码,目前已支持的码制式为EAN-8、EAN-13、UPC-A、UPC-E、Codabar、Code 39、Code 93、Code 128、ITF、QR Code、Data Matrix、PDF417、Aztec。开发者只需要提供字符串、码制式和尺寸要求即可获得相应的码图。

准备

依赖

不管使用哪一种扫码模式,开发者需要先在Project项目下build.gradle中配置插件和仓库地址:

buildscript {
    repositories {
        ...
        maven { url 'https://developer.huawei.com/repo/' }
    }
    dependencies {
        ...
        classpath 'com.huawei.agconnect:agcp:1.4.1.300'

    }
}

allprojects {
    repositories {
        ...
        maven { url 'https://developer.huawei.com/repo/' }
    }
}
...

在app或module的build.gradle中添加扫码依赖:

apply plugin: 'com.android.application'
apply plugin: 'com.huawei.agconnect'
...

android {
   ...
}

dependencies {
     ...
    implementation 'com.huawei.hms:scanplus:1.2.2.300'
    implementation 'com.huawei.agconnect:agconnect-core:1.3.1.300'
   ...
}

权限

无论哪一种扫码模式,都需要先在AndroidManifest中申请权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="...">

    <!--使用特性-->
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <!--相机权限-->
    <uses-permission android:name="android.permission.CAMERA" />
    ...
</manifest>

然后在代码中动态申请权限:

    /**
     * Apply for permissions.
     */
    private void applyPermission(int requestCode) {
        ActivityCompat.requestPermissions(
                this,
                new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE},
                requestCode);
    }

如果权限已申请:

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (permissions == null || grantResults == null) {
            return;
        }
        ...
    }

使用

Default View Mode

默认扫码模式有默认的布局,无需开发者编写布局。

调用
 ScanUtil.startScan(this, REQUEST_CODE_SCAN_ONE, new HmsScanAnalyzerOptions.Creator().create());
扫码结果接收
    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK || data == null) {
            return;
        }
        //Default View
        if (requestCode == REQUEST_CODE_SCAN_ONE) {
            HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT);
            if (obj != null) {
               ScanResultDisplayDialog displayDialog = new ScanResultDisplayDialog(obj,this);
               displayDialog.show(getSupportFragmentManager(),ScanResultDisplayDialog.class.getSimpleName());
            }
        }
   }

HmsScan即为扫码结果对象:

public class HmsScan extends HmsScanBase {
    public static final int ARTICLE_NUMBER_FORM;
    public static final int PURE_TEXT_FORM;
    public static final int ISBN_NUMBER_FORM;
    public HmsScan.EventInfo eventInfo;
    public HmsScan.ContactDetail contactDetail;
    public HmsScan.DriverInfo driverInfo;
    public HmsScan.LinkUrl linkUrl;
    public HmsScan.WiFiConnectionInfo wifiConnectionInfo;
    public HmsScan.SmsContent smsContent;
    public HmsScan.EmailContent emailContent;
    public HmsScan.LocationCoordinate locationCoordinate;
    public static final int EVENT_INFO_FORM;
    public static final int CONTACT_DETAIL_FORM;
    public static final int BOOK_MARK_FORM;
    public HmsScan.BookMarkInfo bookMarkInfo;
    public static final int DRIVER_INFO_FORM;
    public static final int EMAIL_CONTENT_FORM;
    public static final int LOCATION_COORDINATE_FORM;
    public HmsScan.TelPhoneNumber telPhoneNumber;
    public static final int TEL_PHONE_NUMBER_FORM;
    public static final int SMS_FORM;
    public static final int URL_FORM;
    public static final int WIFI_CONNECT_INFO_FORM;
    public static final Creator<HmsScan> CREATOR;
    public static final int VEHICLEINFO_FORM;
    public HmsScan.VehicleInfo vehicleInfo;
    ...
}

扫码结果内容有很多,大致就是二维码或条码的内容是什么,码是什么类型的,扫码结果是什么类型(邮箱、位置、电话、链接...),扫码结果所属类型中具体有哪些信息等。总而言之扫码结果内容很详细,想要的不想要的都有,开发者可以根据结果类型做进一步的处理,这样对开发和用户体验都有很大的帮助,这也是普通扫码所不具备的。

效果预览

在这里插入图片描述

Customized View Mode

布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/fl_custom_scan"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></FrameLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        ...
        <ImageView
            android:id="@+id/scan_area"
            android:layout_width="240dp"
            android:layout_height="240dp"
            android:layout_centerInParent="true"
            android:layout_centerHorizontal="true"
            android:background="@drawable/shape_scan_custom" />
            
    </RelativeLayout>
    ...

</RelativeLayout>

id为fl_custom_scan的FrameLayout是自定义扫码界面容器,id为scan_area的ImageView是中间的扫码框。其他的元素比如返回按钮、标题、手电筒、图片等开发者自行定义。

调用
    @Override
    protected void initView(@Nullable Bundle savedInstanceState) {
        frameLayout = getRoot().findViewById(R.id.fl_custom_scan);
        DisplayMetrics dm = getResources().getDisplayMetrics();
        float density = dm.density;
        mScreenWidth = getResources().getDisplayMetrics().widthPixels;
        mScreenHeight = getResources().getDisplayMetrics().heightPixels;

        int scanFrameSize = (int) (SCAN_FRAME_SIZE * density);
        Rect rect = new Rect();
        rect.left = mScreenWidth / 2 - scanFrameSize / 2;
        rect.right = mScreenWidth / 2 + scanFrameSize / 2;
        rect.top = mScreenHeight / 2 - scanFrameSize / 2;
        rect.bottom = mScreenHeight / 2 + scanFrameSize / 2;

        remoteView = new RemoteView.Builder().setContext(getActivity()).setBoundingBox(rect).setFormat(HmsScan.ALL_SCAN_TYPE).build();
        flushBtn = getRoot().findViewById(R.id.flush_btn);
        remoteView.setOnLightVisibleCallback(new OnLightVisibleCallBack() {
            @Override
            public void onVisibleChanged(boolean visible) {
                if(visible){
                    flushBtn.setVisibility(View.VISIBLE);
                }
            }
        });
        remoteView.setOnResultCallback(new OnResultCallback() {
            @Override
            public void onResult(HmsScan[] result) {
                //Check the result.
                if (result != null && result.length > 0 && result[0] != null && !TextUtils.isEmpty(result[0].getOriginalValue())) {
                    Intent intent = new Intent();
                    intent.putExtra(SCAN_RESULT, result[0]);
                    getActivity().setResult(RESULT_OK, intent);
                    getActivity().finish();
                }
            }
        });

        remoteView.onCreate(savedInstanceState);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        frameLayout.addView(remoteView, params);
        ...
    }

初始化步骤如下:

  1. 通过getResources().getDisplayMetrics()分辨率。
  2. 通过getResources().getDisplayMetrics().widthPixels获取屏幕的宽高。
  3. 通过mScreenWidth / 2 - scanFrameSize / 2等计算实际扫码框的大小。
  4. 创建扫码框及设置扫码格式:RemoteView.Builder().setContext(getActivity()).setBoundingBox(rect).setFormat(HmsScan.ALL_SCAN_TYPE).build()。
  5. 设置闪光灯:remoteView.setOnLightVisibleCallback...。
  6. 扫码结果回调:remoteView.setOnResultCallback(new OnResultCallback() {...}。
  7. 将预览及扫码框添加到布局中:frameLayout.addView(remoteView, params)。

设置OnResultCallback结果回调,重写public void onResult(HmsScan[] result) {},这里的result就是扫码的结果,格式和HmsScan一样。 如果setOnResultCallback回调中可以结果可以直接使用最好,如果不能可以通过回调、intent、handler、广播、eventbus等方法将结果发送到需要的地方。当然还有相册扫码,开发闪关灯等其他功能,这里就不再介绍。

效果预览

在这里插入图片描述

Bitmap Mode

Bitmap模式就是解析资源图片的格式为位图。

布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/fl_bitmap_scan"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#c0c0c0">

        <SurfaceView
            android:id="@+id/sv_bitmap_scan"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    ...
  </FrameLayout>    
调用
  • 相机扫码
  1. 获取相机一帧图像。
YuvImage yuv = new YuvImage(data, ImageFormat.NV21, camera.getParameters().getPreviewSize().width,
        camera.getParameters().getPreviewSize().height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
yuv.compressToJpeg(new Rect(0, 0, camera.getParameters().getPreviewSize().width,
        camera.getParameters().getPreviewSize().height), 100, stream);

上述代码就是将相机的data数组转化成ByteArrayOutputStream流。

  1. 将相机数据转化为Bitmap。
Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.toByteArray().length);
  1. 初始化“HmsScanAnalyzerOptions”,设置支持识别的码制式,设置Bitmap模式为相机扫码模式。
HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE, HmsScan.DATAMATRIX_SCAN_TYPE).setPhotoMode(false).create();

“QRCODE_SCAN_TYPE”和“DATAMATRIX_SCAN_TYPE”表示只扫描QR和Data Matrix的码,可提高扫码速度。setPhotoMode(false):设置Bitmap模式,默认为相机扫码模式。

  1. 结果解析及处理。
HmsScan[] hmsScans = ScanUtil.decodeWithBitmap(BitmapActivity.this, bitmap, options); 
if (hmsScans != null && hmsScans.length > 0 && !TextUtils.isEmpty(hmsScans[0].getOriginalValue())) { 
    //展示扫码结果
    showResult(hmsScans);
}
if (hmsScans != null && hmsScans.length > 0 && TextUtils.isEmpty(hmsScans[0].getOriginalValue()) && hmsScans[0].getZoomValue() != 1.0) {
    //设置相机焦距,相机重新生成Bitmap继续扫码(convertZoomInt()将放大倍数转化为相机接收的焦距参数)
    parameters= camera.getParameters();
    parameters.setZoom(convertZoomInt(hmsScans[0].getZoomValue()));
    camera.setParameters(parameters);
}

hmsScans即为扫码结果,如果扫码不成功可以通过parameters.setZoom调整相机焦距放大倍数重新扫码。

  • 相册扫码
  1. 获取图片并转为Bitmap。
Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), data.getData());
  1. 设置扫码模式为Bitmap模式为图片扫码模式。
HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE, HmsScan.DATAMATRIX_SCAN_TYPE).setPhotoMode(true).create();

setPhotoMode(true)为相册或图片扫码,setPhotoMode(false)为相机扫码。

  1. 结果解析。
HmsScan[] hmsScans = ScanUtil.decodeWithBitmap(BitmapActivity.this, bitmap, options); 

hmsScans为扫码结果,开发者可以进行下一步操作。

效果预览

在这里插入图片描述

MultiProcessor Mode

MultiProcessor 模式是多张二维码同时扫描。

布局

布局和Bitmap布局一样。

调用
  • 同步
  1. 设置支持识别的码制式。
HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE, HmsScan.DATAMATRIX_SCAN_TYPE).create();

这一步可以忽略,这只是针对有特殊码的需求。

  1. 初始化“HmsScanAnalyzer”对象(解码对象)。
HmsScanAnalyzer barcodeDetector = new HmsScanAnalyzer(options);
  1. 将图像信息转换为“MLFrame”,“MLFrame”为ML Kit封装的图像信息类。
MLFrame image = MLFrame.fromBitmap(bitmap);
  1. 获取扫码结果。
SparseArray<HmsScan> result = barcodeDetector.analyseFrame(image);

调用“HmsScanAnalyzer”对象的“analyseFrame”扫码方法发起扫码请求并获取扫码结果。扫码结果为HmsScan数据结构,HmsScan数据结构的信息请参见码值解析

  • 异步 1,2,3步和同步的第1,2,3一样。
  1. 接收扫码结果。
barcodeDetector.analyzInAsyn(image).addOnSuccessListener(new OnSuccessListener<List<HmsScan>>() { 
    @Override 
    public void onSuccess(List<HmsScan> hmsScans) { 
        if (hmsScans != null && hmsScans.size() > 0) { 
            //展示扫码结果
            showResult(hmsScans);
        } 
    } 
}).addOnFailureListener(new OnFailureListener() { 
    @Override 
    public void onFailure(Exception e) { 
        Log.w(TAG, e); 
    } 
});

调用“HmsScanAnalyzer”对象的“analyzInAsyn”扫码方法发起扫码请求,同时订阅扫码结果事件来接收扫码结果。扫码结果为HmsScan数据结构。异步总是要设置成功与失败回调。

效果预览

在这里插入图片描述

生成码

  1. 根据实际需求设置生成码的字符串值,指定生成码图的宽度和高度,指定码制式(支持的码制式参见Scan Kit支持的码制式)。
String content = "QR Code Content"; 
int type = HmsScan.QRCODE_SCAN_TYPE;
int width = 400; 
int height = 400;

2.初始化“HmsBuildBitmapOption”,设置可选参数。 (可选)

HmsBuildBitmapOption options = new HmsBuildBitmapOption.Creator().setBitmapBackgroundColor(Color.RED).setBitmapColor(Color.BLUE).setBitmapMargin(3).create();
  • setBitmapBackgroundColor():设置码图背景色,如果不调用,默认背景色为白色(Color.WHITE)。
  • setBitmapColor():设置码图颜色,如果不调用,默认码图颜色为黑色(Color.BLACK)。
  • setBitmapMargin():设置码图边框宽度,如果不调用,默认码图边框宽度为1。
  1. 调用“buildBitmap”接口生成码。
try {
    //如果未设置HmsBuildBitmapOption对象,生成二维码参数options置null”
    Bitmap qrBitmap = ScanUtil.buildBitmap(content, type, width, height, options);
} catch (WriterException e) {
    Log.w("buildBitmap", e);
}
  1. 预览生成的二维码。 在这里插入图片描述

总结

  • 华为扫码服务模式多样,扫码速度快,对扫码环境的要求很低,很适合在复杂环境及特殊场景下使用。
  • 扫码结果详细,有利于开发者对扫码分类及异常处理。
  • 使用简单,有利于节省开发时间。
  • 扫码服务在彩色及不规则二维码上的表现不错。
  • 当手机镜头和码出现平行夹角的时候,识别速度还是有点慢,后续有待优化。

说明

  • 文中相关素材来自于统一扫码服务指南
  • 文中使用的扫码资源图片是随机百度的。
  • 文中扫码使用只是对主要的代码进行说明,关于生命周期等一些细节请参考统一扫码服务指南
  • 文中代码及效果只在Android手机测试,不能代表iOS。
  • 若有侵权或错误,请发送邮件至alphabetadata@163.com