跳转至

QR 码扫描

本文档介绍了 QR 码扫描功能的实现方式及其如何暴露给 WebView 层,内容包括:

  • JavaScript 用于启动扫描的桥接 API
  • 权限处理方式
  • 如何启动华为 ScanKit Activity
  • 扫描结果如何回传给 JavaScript

QR 扫描功能在 BaseWebViewActivity 中实现,并通过 bridgeWebView 暴露给 WebView。

概述

QR 扫描流程如下:

┌──────────┐
│   START  │
└────┬─────┘
     │
     ▼
┌─────────────────────────┐
│ Check Permissions       │
│ - CAMERA                │
│ - READ_MEDIA            │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ Register Callback       │
│ scanQrcodeResultCallback│
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ startQrCodeScan(999)    │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ Open Camera Scanner     │
│                         │
│  ┌─────────────────┐    │
│  │  [QR Frame]     │    │
│  │                 │    │
│  │  Point camera   │    │
│  │  at QR code     │    │
│  └─────────────────┘    │
└────┬────────────────────┘
     │
     │ (User scans or cancels)
     │
     ├────────────┬─────────────┐
     ▼            ▼             ▼
  Success      Cancel         Error
     │
     ▼
┌─────────────────────────┐
│ Decode QR Data          │
│ Extract value           │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ scanQrcodeResultCallback│
│ {                       │
│   value: "QR_CONTENT",  │
│   requestCode: 999      │
│ }                       │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ Process QR Data         │
│ in JavaScript           │
│ - URL redirect          │
│ - Parse JSON            │
│ - etc.                  │
└─────────────────────────┘
     │
     ▼
┌──────────┐
│   END    │
└──────────┘
  1. JavaScript 调用原生桥接方法 startQrCodeScan
  2. 原生代码检查并请求必要的权限(摄像头等)。
  3. 权限授予后,原生配置华为 ScanKit 并启动 ScanKit Activity。
  4. 用户扫描 QR 码(或取消)。
  5. BaseWebViewActivity.onActivityResult() 接收结果,提取扫描值,并通过 scanQrcodeResultCallBack 将其回传给 JavaScript。

整个流程完全由事件驱动:WebView 触发扫描,并通过回调异步接收结果。

原生–WebView 桥接 API

startQrCodeScan(JS → 原生)

桥接处理器名称:startQrCodeScan 定义于:BaseWebViewActivity 内的 registerMethod()

原生侧签名:

bridgeWebView.registerHandler("startQrCodeScan", new BridgeHandler() {
    @Override
    public void handler(String requestCode, CallBackFunction function) {
        // ...
    }
});

参数

requestCode(String)

- 可选,由 JavaScript 发送。
- 如果为空,`REQUEST_CODE_SCAN_ONE` 默认为 0。
- 如果非空,则解析为整数并存储在 `REQUEST_CODE_SCAN_ONE` 中。
- 允许 JS 侧在需要时区分多个扫描请求。

原生逻辑示例:

REQUEST_CODE_SCAN_ONE = TextUtils.isEmpty(requestCode)
        ? 0
        : Integer.valueOf(requestCode);

立即原生行为

handler(...) 内部,原生代码:

  1. 使用 XXPermissions(配合自定义 PermissionInterceptor)请求运行时权限。
  2. 如果未授予所有必需权限,立即向 JS 回调权限错误:
    function.onCallBack(gson.toJson(Result.fail(PERMISSION_ERROR, false)));
    return;
    
  3. 如果权限已授予,则构建华为 ScanKit 配置并启动扫描 Activity。

权限处理

通过 XXPermissions 请求权限:

XXPermissions.with(BaseWebViewActivity.this)
    // (Camera and any related permissions are configured here)
    .interceptor(new PermissionInterceptor())
    .request(new OnPermissionCallback() {
        @Override
        public void onGranted(@NonNull List<String> permissions, boolean allGranted) {
            if (!allGranted) {
                function.onCallBack(gson.toJson(Result.fail(PERMISSION_ERROR, false)));
                return;
            }
            // Permissions granted → proceed to launch scanner
            // ...
        }
    });

关键要点:

  • 在启动任何扫描 UI 之前请求权限。
  • 如果用户拒绝任何必需权限,WebView 收到包含 Result.fail(PERMISSION_ERROR, false) 的响应,且不会启动扫描 Activity。
  • 实际权限列表(摄像头、存储等)在原生代码中配置,可根据需要调整。

启动扫描 UI(华为 ScanKit)

权限授予后,代码构建 HmsScanAnalyzerOptions 并启动华为 ScanKit Activity:

HmsScanAnalyzerOptions.Creator creator = new HmsScanAnalyzerOptions.Creator();

// Accept all types of barcodes / QR codes
creator.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE);

// Show the built-in scan guide overlay
creator.setShowGuide(true);

// Allow scanning from gallery (photo mode)
creator.setPhotoMode(true);

HmsScanAnalyzerOptions hmsScanAnalyzerOptions = creator.create();

// Launch ScanKit activity
Intent intent = new Intent(BaseWebViewActivity.this, ScanKitActivity.class);
if (intent != null) {
    intent.putExtra("ScanFormatValue", hmsScanAnalyzerOptions.mode);
}

startActivityForResult(intent, REQUEST_CODE_SCAN_ONE);

注意事项:

  • HmsScan.ALL_SCAN_TYPE 启用多种条形码格式的识别,而不仅仅是 QR 码。
  • setShowGuide(true) 在摄像头预览上显示引导叠加层,帮助用户对准。
  • setPhotoMode(true) 允许用户从相册中选择现有图片进行扫描。
  • REQUEST_CODE_SCAN_ONE 稍后在 onActivityResult 中用于识别结果。

如果在构建选项或启动 Activity 时发生异常,将被捕获并记录:

} 
catch (Exception e) {
    e.printStackTrace();
}

接收扫描结果(原生)

扫描结果在 onActivityResult 中处理:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_CODE_SCAN_ONE && resultCode == RESULT_OK) {
        HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT);
        Toaster.show(obj.originalValue);

        if (obj != null) {
            Map<String, Object> map = new HashMap<>();
            map.put("value", obj.originalValue);
            map.put("requestCode", REQUEST_CODE_SCAN_ONE);

            bridgeWebView.callHandler("scanQrcodeResultCallBack",
                    gson.toJson(Result.ok(map)),
                    new CallBackFunction() {
                        @Override
                        public void onCallBack(String data) {
                        }
                    });
        } else {
            bridgeWebView.callHandler("scanQrcodeResultCallBack",
                    gson.toJson(Result.fail()),
                    new CallBackFunction() {
                        @Override
                        public void onCallBack(String data) {
                        }
                    });
        }
    }

    // ... (other onActivityResult branches: picture choose, OCR, etc.)
}

重要细节

  • 只有当 requestCode == REQUEST_CODE_SCAN_ONEresultCode == RESULT_OK 时,才将其视为有效的扫描结果。
  • HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT); 从 Intent 中获取 HmsScan 结果对象。
  • obj.originalValue 包含 QR 码/条形码中编码的字符串值。
  • 显示一个简短的 Toast(Toaster.show(obj.originalValue);)用于调试/快速反馈。
  • 如果 obj 非空:

    • 创建一个 map,包含:
      • "value" → 扫描到的字符串(obj.originalValue
      • "requestCode" → 使用的请求码(使 JS 可以关联响应)
    • 将 map 用 Result.ok(map) 包装后通过 scanQrcodeResultCallBack 发送回 JS。
  • 如果 obj 为空:

    • 原生调用 scanQrcodeResultCallBack 并传入 Result.fail()

取消处理:

- 如果用户取消 ScanKit 界面,`resultCode` 将不是 `RESULT_OK`,因此 QR 分支被跳过。
- 此时不发送成功负载;JS 侧应通过将"未收到结果"视为已取消操作来处理,或根据需要添加单独的机制。

JavaScript 侧使用方法

1. 注册结果回调

在 WebView JavaScript 中,您必须注册处理器以接收扫描结果

bridgeWebView.registerHandler("scanQrcodeResultCallBack", function (data) {
  const res = JSON.parse(data);

  // Depending on your Result implementation, this may look like:
  // { success: true/false, code: ..., data: { value, requestCode } }

  if (res.success) {
    const value = res.data.value;
    const reqCode = res.data.requestCode;
    console.log("QR scan success:", value, "(req:", reqCode, ")");

    // TODO: handle the scanned value (navigate, fill form, etc.)
  } else {
    console.warn("QR scan failed", res);
    // TODO: show error message or retry option
  }
});

2. 从 JS 启动 QR 扫描

当您想要开始扫描时:

function startQrScan() {
  const REQUEST_CODE_QR = 1001; // any integer you want to track this call

  bridgeWebView.callHandler(
    "startQrCodeScan",
    String(REQUEST_CODE_QR), // this becomes requestCode in native
    function (response) {
      // Initial response from native (permission / immediate errors)
      const res = JSON.parse(response || "{}");

      if (res.success === false) {
        console.error("Failed to start QR scan:", res);
        // Probably permission denied or some immediate error.
      } else {
        // In most cases, you simply wait for scanQrcodeResultCallBack.
        console.log("QR scan started; waiting for result...");
      }
    }
  );
}

从 JS 角度看的流程:

  1. 调用 startQrCodeScan
  2. 可选地检查立即响应(用于权限错误)。
  3. 等待包含最终结果的 scanQrcodeResultCallBack

结果负载结构

所有响应在发送给 JS 之前都用内部 Result 类包装,例如:

  • 成功(概念结构):

        {
      "success": true,
      "code": 0,
      "data": {
        "value": "scanned-qr-content",
        "requestCode": 1001
      }
    }
    

  • 失败时

    {
      "success": false,
      "code": "<error-code>",
      "message": "<human-readable message>"
    }
    

确切的键(codemessage 等)取决于您的 Result 实现,但重要的是 data.value 保存扫描到的字符串,data.requestCode 让您将结果映射到特定的扫描请求。

错误处理与边界情况

  • 权限被拒绝

    • startQrCodeScan 立即返回 Result.fail(PERMISSION_ERROR, false)
    • 扫描 UI 从不打开。
    • JS 应显示消息,要求用户授予摄像头权限。
  • 扫描返回空结果(obj == null

    • scanQrcodeResultCallBackResult.fail() 调用。
    • JS 应将此视为"扫描失败"并允许重试。
  • 用户取消 ScanKit

    • resultCode 不是 RESULT_OK
    • onActivityResult 中跳过 QR 分支。
    • 不发送成功负载;JS 应将此处理为"无结果"(如通过超时或用户流程)。
  • 启动 ScanKit 时发生意外异常

    • 目前仅通过 e.printStackTrace() 记录。
    • 您可能希望扩展此功能以向 JS 发送失败的 Result,以改善用户体验。

总结

  • QR 扫描实现向 WebView 提供了简洁的高级 API:

    • JS 入口点:startQrCodeScan
    • 原生扫描器:通过 ScanKitActivity 使用华为 ScanKit
    • 向 JS 的结果回调:scanQrcodeResultCallBack,负载包含扫描值和 requestCode 的 Result 封装对象。

这将所有摄像头和 ScanKit 细节保留在原生层,同时保持 WebView 集成简单可预测。