大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

在 HarmonyOS 应用开发中,二维码与条形码扫描是一个极高频的功能需求。HarmonyOS 提供了 Scan Kit(扫码服务),开发者可以通过极少的代码接入系统预置的标准扫码界面(Default View)。然而,在许多真实的业务场景中,标准界面可能无法完全满足要求——例如需要在扫码框周围嵌入自定义交互控件、展示特定的品牌视觉元素、实现连续扫码商品入库,或者对扫码区域(ROI)进行精准限定。

本文将基于 HarmonyOS 的 Scan Kit(扫码服务) 中的 Customized View(自定义界面) 模式,完整剖析如何自主掌控相机预览、识别区域配置、闪光灯状态切换、连续扫码防抖以及页面生命周期内的资源释放。

一、标准模式与自定义模式的区别

在接入 Scan Kit 时,通常有两种技术选型路径:

  1. 标准扫码模式(Default View):由 Scan Kit 统一提供扫码 UI 界面,包含相机预览流管理、扫码框渲染、相册导入入口、闪光灯控制按钮以及返回键等。调用方式极简(通过 scanBarcode.startScan 调起全屏扫码页),但 UI 样式和交互逻辑被系统严格封包,无法进行自由排版。
  2. 自定义扫码模式(Customized View):Scan Kit 仅负责底层的图像分析、算法识别与相机流数据处理,而将 UI 渲染图层(基于 XComponent)以及控件控制权交还给开发者。
维度标准扫码模式 (Default View)自定义扫码模式 (Customized View)
UI 自由度固定系统样式,不支持改动完全自由,由 ArkUI 布局控制
相机预览载体系统内部控制页面XComponent 提供 Surface
识别区域控制全屏或系统默认区域可通过 scanFrame 属性精准限制
连续扫码支持扫描到一次即关闭或返回支持不间断持续识别与多次结果回调
适用场景通用扫码登录、打卡、跳转链接仓库盘点、定制化扫描框、多业务控件集成

二、准备工作与权限配置

自定义扫码依赖设备的相机硬件能力,因此必须在工程中配置相机权限 ohos.permission.CAMERA,并在运行期动态向用户申请授权。

1. 配置文件权限声明

在模块的 src/main/module.json5 文件中追加 permissions 声明:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_permission_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}

2. 动态申请相机权限

在拉起自定义扫码视图前,需要先校验并动态请求权限:

import { abilityAccessCtrl, common, PermissionRequestResult, Permissions } from '@kit.AbilityKit';

async function requestCameraPermission(context: common.UIAbilityContext): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  const permissions: Array<Permissions> = ['ohos.permission.CAMERA'];

  try {
    const grantStatus = await atManager.requestPermissionsFromUser(context, permissions);
    return grantStatus.authResults[0] === 0; // 0 表示 PERMISSION_GRANTED
  } catch (error) {
    return false;
  }
}

三、核心组件与逻辑流图解

自定义扫码的核心运行逻辑可以分为四个关键阶段:组件挂载与 Surface 准备 -> Scan Kit 初始化与绑定 -> 识别结果回调与防抖 -> 生命周期终止与资源解绑。

+-------------------------------------------------------------------+
|                        ArkUI Page Component                       |
|                                                                   |
|  +-------------------------------------------------------------+  |
|  |                       XComponent                            |  |
|  |  (提供 SurfaceId,作为 Camera 预览流渲染载体)                     |  |
|  +------------------------------+------------------------------+  |
|                                 |                                 |
|                                 v (onLoad 获取 surfaceId)          |
|  +-------------------------------------------------------------+  |
|  |                      Scan Kit Service                       |  |
|  |  1. customScan.init(options)                                |  |
|  |  2. customScan.start(viewControl)                             |  |
|  |  3. customScan.on('result', callback)                         |  |
|  +------------------------------+------------------------------+  |
|                                 |                                 |
|                                 v (生命周期控制: stop & release)     |
|  +-------------------------------------------------------------+  |
|  |                   Camera & Hardware Resource                |  |
|  +-------------------------------------------------------------+  |
+-------------------------------------------------------------------+

四、自定义扫码页面完整 ArkTS 代码实现

下面提供一个包含“相机预览”、“特定识别框定位”、“闪光灯控制”、“连续扫码防抖”、“防频繁触发机制”以及“严格资源释放”的标准最小实践方案。

import { customScan, scanBarcode } from '@kit.ScanKit';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct CustomScanPage {
  // XComponent 控制器,用于获取系统渲染 Surface
  private xComponentController: XComponentController = new XComponentController();
  // 相机预览区域 ID
  private surfaceId: string = '';
  // 预览区容器宽高(单位:px)
  @State cameraCompWidth: number = 360;
  @State cameraCompHeight: number = 640;
  // 闪光灯状态指示
  @State isFlashlightOpen: boolean = false;
  // 扫码结果防抖标记位
  private isProcessingResult: boolean = false;
  // 上次成功解析的时间戳(用于连续扫码冷却间隔)
  private lastScanTimestamp: number = 0;
  // 冷却防抖阈值(单位:毫秒)
  private readonly DEBOUNCE_INTERVAL: number = 1500;

  aboutToDisappear() {
    // 页面销毁前,确保扫码引擎与相机资源解绑释放
    this.releaseScanServices();
  }

  onPageHide() {
    // 页面不可见时(如切后台、跳转新页),暂停扫码逻辑以节省功耗
    this.stopScanServices();
  }

  onPageShow() {
    // 页面恢复可见时,重新拉起扫码引擎
    if (this.surfaceId) {
      this.startScanServices();
    }
  }

  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      // 1. 底层相机预览层(通过 XComponent 承载)
      XComponent({
        id: 'componentId',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .onLoad(() => {
          // 获取当前 XComponent 生成的 Surface ID
          this.surfaceId = this.xComponentController.getXComponentSurfaceId();
          // 开始初始化与拉起扫码
          this.initAndStartScan();
        })
        .onDestroy(() => {
          this.releaseScanServices();
        })
        .width('100%')
        .height('100%')

      // 2. 开发者自定义 UI 叠加层:扫描框与视觉辅助线
      this.buildCustomScanOverlay()

      // 3. 顶部导航与控制按钮区
      this.buildHeaderControls()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }

  // 渲染扫描框叠加层
  @Builder
  buildCustomScanOverlay() {
    Stack() {
      // 扫码框外围遮罩效果及边框渲染
      Column()
        .width(280)
        .height(280)
        .border({ width: 2, color: '#007DFF', style: BorderStyle.Solid })
        .borderRadius(16)

      Text('将二维码/条形码对准框内')
        .fontColor('#FFFFFF')
        .fontSize(14)
        .margin({ top: 320 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  // 渲染顶部导航及控制按钮
  @Builder
  buildHeaderControls() {
    Row() {
      // 闪光灯切换按钮
      Button(this.isFlashlightOpen ? '关闭闪光灯' : '开启闪光灯')
        .fontSize(14)
        .fontColor(Color.White)
        .backgroundColor('rgba(0,0,0,0.5)')
        .onClick(() => {
          this.toggleFlashlight();
        })
    }
    .width('100%')
    .padding({ top: 48, left: 24, right: 24 })
    .justifyContent(FlexAlign.End)
  }

  // 初始化与开启扫码流程
  private initAndStartScan() {
    // 设置扫码参数:解析的条码类型与扫码识别区域(ROI)
    const options: scanBarcode.ScanOptions = {
      scanTypes: [scanBarcode.ScanType.ALL],
      // 可选:设置实际识别区域 [x, y, width, height](单位:px),提升特定框内的解析速度
      scanFrame: {
        x: 40,
        y: 180,
        width: 280,
        height: 280
      }
    };

    try {
      // 初始化自定义扫码引擎
      customScan.init(options);

      // 注册识别结果监听事件(连续扫码的核心)
      customScan.on('result', (result: Array<scanBarcode.ScanResult>) => {
        this.handleScanResults(result);
      });

      // 启动相机预览流并指定渲染载体
      this.startScanServices();
    } catch (error) {
      const err = error as BusinessError;
      console.error(`Scan Kit init failed, code: ${err.code}, message: ${err.message}`);
    }
  }

  // 启动扫码硬件设备
  private startScanServices() {
    const viewControl: customScan.ViewControl = {
      width: this.cameraCompWidth,
      height: this.cameraCompHeight,
      surfaceId: this.surfaceId
    };

    customScan.start(viewControl).then(() => {
      console.info('CustomScan start succeeded.');
    }).catch((error: BusinessError) => {
      console.error(`CustomScan start failed, code: ${error.code}, message: ${error.message}`);
    });
  }

  // 停止扫码(暂停硬件采集)
  private stopScanServices() {
    try {
      customScan.stop().then(() => {
        console.info('CustomScan stop succeeded.');
      }).catch((error: BusinessError) => {
        console.error(`CustomScan stop failed, code: ${error.code}, message: ${error.message}`);
      });
    } catch (error) {
      console.error('CustomScan stop execution error.');
    }
  }

  // 完全释放扫码资源
  private releaseScanServices() {
    try {
      // 移除监听
      customScan.off('result');
      // 停止并释放底层相机与算法句柄
      customScan.stop();
      customScan.release().then(() => {
        console.info('CustomScan release succeeded.');
      }).catch((error: BusinessError) => {
        console.error(`CustomScan release failed, code: ${error.code}, message: ${error.message}`);
      });
    } catch (error) {
      console.error('CustomScan release execution error.');
    }
  }

  // 闪光灯控制
  private toggleFlashlight() {
    try {
      const currentStatus = customScan.getFlashlightStatus();
      if (currentStatus) {
        customScan.closeFlashlight();
        this.isFlashlightOpen = false;
      } else {
        customScan.openFlashlight();
        this.isFlashlightOpen = true;
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`Flashlight toggle failed, code: ${err.code}, message: ${err.message}`);
    }
  }

  // 扫码结果解析与连续防抖逻辑
  private handleScanResults(results: Array<scanBarcode.ScanResult>) {
    if (!results || results.length === 0) {
      return;
    }

    const now = Date.now();
    // 防抖机制 1:时间戳间隔判断
    if (now - this.lastScanTimestamp < this.DEBOUNCE_INTERVAL) {
      return;
    }

    // 防抖机制 2:并发事务锁控制
    if (this.isProcessingResult) {
      return;
    }

    this.isProcessingResult = true;
    this.lastScanTimestamp = now;

    // 提取解析出的二维码真实内容
    const scannedValue = results[0].originalValue;
    const scanType = results[0].scanType;

    promptAction.showToast({
      message: `识别结果: ${scannedValue}`,
      duration: 2000
    });

    // 模拟业务处理流程(如网络请求入库)
    setTimeout(() => {
      this.isProcessingResult = false;
    }, 1000);
  }
}

五、关键技术点拆解与代码分析

1. 相机预览与 XComponent 的绑定机制

在 ArkUI 中,XComponent 组件用于绘制 Native 层或者硬件流传递的 Surface 内容。
定义 XComponentType.SURFACE 后,组件会在挂载成功后触发 onLoad 回调,并通过控制器返回一个系统唯一的 surfaceId。

Scan Kit 内部封装了 Camera Kit 的底层交互,开发者无需手动调用相机采集 API,只需把生成的 surfaceId 封装入 customScan.ViewControl 并传给 customScan.start(),Scan Kit 就会自动将相机预览流正确输出到指定的 UI 区域内。

2. 精准识别区域设置(scanFrame)

为了避免扫码界面背景中存在其他条码干涉,或者为了提升算法在特定小范围内的检测速率,可通过 scanBarcode.ScanOptions 中的 scanFrame 参数来强行限定识别边界:

const options: scanBarcode.ScanOptions = {
  scanTypes: [scanBarcode.ScanType.ALL],
  scanFrame: {
    x: 40,      // 识别区左上角 X 轴坐标(px)
    y: 180,     // 识别区左上角 Y 轴坐标(px)
    width: 280, // 识别区宽度(px)
    height: 280 // 识别区高度(px)
  }
};

若未设置 scanFrame,Scan Kit 默认会对相机输入的全图进行全量特征点扫描。引入 scanFrame 后,算法引擎将裁剪图像帧只针对感兴趣区域(ROI)计算,极大地减小了端侧设备计算开销。

3. 连续扫码场景下的防抖控制

标准扫码模式在识别到条码后会自动关页或停发信号,而自定义模式使用 customScan.on('result', ...) 持续监听。在相机持续对准二维码时,底层算法引擎可能以 15~30 fps 的频率连续抛出 scanBarcode.ScanResult 数组。

如果直接在此回调中执行数据弹窗、音频播放或 API 网络请求,将造成严重的页面卡顿与重复逻辑处理。在代码设计中,采用了双重防护逻辑:

  1. 时间戳阈值校验(DEBOUNCE_INTERVAL):强行限定两次处理之间必须相隔固定的时间间隔(如 1.5 秒)。
  2. 状态锁限制(isProcessingResult):在异步业务(如提交数据库或请求服务器)尚未执行完毕时,忽略任何新的识别回调。

4. 闪光灯控制接口的安全性

Scan Kit 提供了简洁的闪光灯控制方法:

  • customScan.getFlashlightStatus():查询当前设备闪光灯是否点亮。
  • customScan.openFlashlight():开启闪光灯。
  • customScan.closeFlashlight():关闭闪光灯。

注意点:调用闪光灯接口必须在 customScan.start() 成功启动相机预览流之后。如果相机未处于激活状态,直接调用闪光灯控制接口会抛出系统异常(BusinessError)。

六、生命周期管理与资源释放

对相机硬件与图像识别算法引擎资源的合理管控,是保证应用性能稳定、避免耗电异常以及防止硬件占用的重中之重。开发者需要重点关注以下三种场景:

【建议插图3:扫码页面生命周期与 Scan Kit API 调用对应关系图】

1. 页面隐藏(onPageHide)与恢复(onPageShow)

当用户点击应用切换、拉起下拉通知栏或者页面跳转时,当前页面触发 onPageHide。此时相机不再需要画面渲染,应当立即调用 customScan.stop() 停止硬件采集流,以大幅降低设备功耗并减少发热。当页面重新回到前台(onPageShow)时,再次调用 customScan.start() 恢复预览。

2. 页面销毁(aboutToDisappear / onDestroy)

在 UI 组件被销毁前,必须按照固定顺序释放资源:

  1. customScan.off('result'):解绑识别结果的回调监听,避免内存泄漏;
  2. customScan.stop():停止相机硬件流;
  3. customScan.release():释放 Scan Kit 底层初始化占用的内存与句柄。

若未及时调用 release(),后续重新打开扫码页面时,可能因为底层的 Camera 硬件资源仍然处于占用独占状态,导致预览区域出现黑屏现象。

七、常见问题与排查清单

在接入自定义扫码时,如果遇到异常表现,可以按照以下清单快速检查排查:

  1. 预览区黑屏,不显示相机画面

    • 检查权限:确认 module.json5 中是否声明 ohos.permission.CAMERA,且运行期动态申请已获得批准。
    • 检查 Surface ID:确认 XComponentController.getXComponentSurfaceId() 是否成功获取到了有效字符串,且初始化动作是在 XComponent 的 onLoad 回调内触发。
    • 检查硬件占用:确认前一个扫码页面或系统的其他应用没有强行独占相机资源,确保上一生命周期正确执行了 customScan.release()。
  2. 扫描成功率低或条形码识别困难

    • 检查 scanFrame 尺寸与位置:设定的像素坐标是否超出了 XComponent 实际渲染区域的绝对像素范围;
    • 检查比例与分辨率:ViewControl 的 width 与 height 应保持与 XComponent 逻辑尺寸比例一致,避免图像画面拉伸失真导致特征点识别失败。
  3. 连续重复触发多次逻辑

    • 检查监听注销:确认是否在页面多次打开时重复绑定了 customScan.on('result') 监听;
    • 检查防抖变量:确认 handleScanResults 中是否有控制并更新时间戳或状态锁逻辑。

八、开发经验总结

基于 HarmonyOS Scan Kit 开发自定义扫码能力,本质上是在UI 渲染自由度与底层算法高性能之间达成平衡。

  • 在样式定制要求较高的项目中,优先采用 XComponent + customScan 的自组装方案,能够在完全掌控界面交互的同时,获得系统级硬件加速的识别效率。
  • 牢记生命周期对齐原则:相机与算法属于高功耗敏感资源,必须严格遵循“显示即启动(start)、不可见即停止(stop)、销毁即释放(release)”的管理铁律。
  • 对于连续扫码场景,务必在 ArkTS 应用层增加时序防抖与并发状态控制,杜绝连续回调导致的业务重复提交与界面卡顿。

你所在的业务场景是在做定制扫描框,还是需要连续扫码盘点?在窗口尺寸变化或者应用切后台时,遇到了哪些关于 Surface 管理的问题?欢迎在评论区留言交流探讨。

参考资料

  1. 《Scan Kit 开发指南 - 自定义界面扫码》华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/scan-customizedized-view-0000001774120158
  2. 《Scan Kit API 参考 - ScanBarcode》华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/scan-scanbarcode-0000001774280802
  3. 《Scan Kit API 参考 - CustomScan》华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/scan-customscan-0000001774121282

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐