1***@qq.com
1***@qq.com
  • 发布:2025-11-11 20:33
  • 更新:2025-11-11 20:33
  • 阅读:14

【鸿蒙征文】uniapp 实现鸿蒙自定义扫码界面

分类:鸿蒙Next

有些应用场景,需要使用自定义扫码的界面,添加自定义的布局和功能,相比 uniapp 自带的 uni.scanCode ,布局更加自由,完全使用鸿蒙开发的布局能力。

官网 自定义界面扫码 文档提供了明确的接入方案,这里使用 Uniapp 提供的嵌入原生组件来完成,思路都是相通的,回一个就会所有的原生组件方案了。

涉及到鸿蒙原生的组件、原生能力就得使用 uniapp 嵌入原生组件了,详细看文档 嵌入鸿蒙原生组件

整体思路比较简单

  • 测试鸿蒙原生工程写法,完成布局逻辑、扫码逻辑编写,可直接使用官网提供的 demo
  • 编写 uts 代码,引入 ets 文件并完成代码封装
  • 页面中使用

了解原生写法

一图胜千言:

  • 核心逻辑:权限处理-拉齐页面-释放扫码资源
  • 核心方法 init/start/stop/release等

官网中提供了一个示例,

代码相对清晰:

  • 定义基础 state
  • 定义权限请求
  • 定义 customScan.init 完成初始化,并处理扫码逻辑
  • 布局时候添加了闪光灯的按钮

需要注意的是,在核心 ets 代码之外有一个固定的封装

@Component  
struct HarmoyScanLayoutComponent{}  

@Builder  
function ScanLayoutBuilder(options: ScanLayoutBuilderOptions) {  
  HarmoyScanLayoutComponent({  
    enableMultiMode: options.enableMultiMode ?? true,  
    enableAlbum: options.enableAlbum ?? true,  
    onScanResult: options?.on?.get?.('scanresult'),  
    onFlashLightChange: options?.on?.get?.('flashlightchange')  
  })  
    .width(options.width)  
    .height(options.height)  
}  

defineNativeEmbed('harmoy-scan-layout', {  
  builder: ScanLayoutBuilder  
})

参考 uniapp 文档了解即可。这里定义 Component 和 Builder 是固定的写法。

编写 uts 插件代码

HBuilderX 中新建 uni api 插件,定位到 app-harmony 文件夹。新建 index.utsscan.ets

index.uts 比较简单,就一行代码

import './scan.ets'

scan.ets 完整代码简附录。

这里用到了摄像头的权限,因此需要使用 module.json5

{  
  "module": {  
    "requestPermissions": [  
      {  
        "name": "ohos.permission.CAMERA",  
        "usedScene": {  
          "when": "inuse"  
        },  
        "reason": "$string:EntryAbility_desc"  
      }  
    ]  
  }  
}

这里忽略了 interface.uts 和 unierror.uts 的代码。

完整的目录结构是

--app-harmony  
----index.uts  
----scan.ets  
--interface.uts  
--unierror.uts

页面中使用

<template>  
    <view>  
        <embed class="scan-area" tag="harmoy-scan-layout" :options="options" @scanresult="onScanResult"  
            @flashlightchange="onFlashChange" />  
    </view>  
</template>  

<script>  
    import '@/uni_modules/harmoy-scan-layout'  
    export default {  
        data() {  
            return {  
                options: {  
                    // 可选:是否开启多码识别(默认 true)  
                    enableMultiMode: true,  
                    // 可选:是否允许相册识别(默认 true)  
                    enableAlbum: true  
                }  
            }  
        },  
        methods: {  
            onScanResult(e) {  
                uni.showToast({  
                    title:JSON.stringify(e)  
                })  
                console.log('scanresult', e.detail.results)  
            },  
            onFlashChange(e) {  
                console.log('flashlight enabled:', e.detail.enabled)  
            }  
        }  
    }  
</script>  

<style scoped>  
    .scan-area {  
        display: block;  
        width: 100%;  
        height: 90vh;  
    }  
</style>

附录 scan.ets 代码

import abilityAccessCtrl from '@ohos.abilityAccessCtrl'  
import common from '@ohos.app.ability.common'  
import { customScan, scanBarcode, scanCore } from '@kit.ScanKit';  
import { BusinessError } from '@kit.BasicServicesKit';  

const TAG: string = '[harmoy-scan-layout]'  

interface ScanLayoutBuilderOptions extends NativeEmbedBuilderOptions {  
  // 是否开启多码识别(默认: true)  
  enableMultiMode?: boolean  
  // 是否允许从相册识别(默认: true)  
  enableAlbum?: boolean  
}  

interface ScanResultEventDetail {  
  results: Array<scanBarcode.ScanResult>  
}  

@Component  
struct HarmoyScanLayoutComponent {  
  // 组件参数  
  @Prop enableMultiMode: boolean  
  @Prop enableAlbum: boolean  
  // 事件回调(通过 options.on 传入)  
  onScanResult?: Function  
  onFlashLightChange?: Function  
  // 状态  
  @State userGrant: boolean = false  
  @State surfaceId: string = ''  
  @State isShowBack: boolean = false  
  @State isFlashLightEnable: boolean = false  
  @State isSensorLight: boolean = false  
  @State cameraHeight: number = 640  
  @State cameraWidth: number = 360  
  @State offsetX: number = 0  
  @State offsetY: number = 0  
  @State zoomValue: number = 1  
  @State setZoomValue: number = 1  
  @State scaleValue: number = 1  
  @State pinchValue: number = 1  
  @State displayHeight: number = 0  
  @State displayWidth: number = 0  
  @State scanResult: Array<scanBarcode.ScanResult> = []  
  canIUse: boolean = canIUse("SystemCapability.Multimedia.Scan.ScanBarcode")  
  private mXComponentController: XComponentController = new XComponentController()  

  aboutToAppear(): void {  
    // 生命周期开始:申请权限并初始化  
    (async () => {  
      await this.requestCameraPermission()  
      this.setDisplay()  
      try {  
        const options: scanBarcode.ScanOptions = {  
          // 与平台枚举对齐,由内部适配完成  
          scanTypes: [scanCore.ScanType.ALL],  

          enableMultiMode: this.enableMultiMode ?? true,  
          enableAlbum: this.enableAlbum ?? true  
        }  
        if (this.canIUse) {  
          customScan.init(options)  
        } else {  
          // Fallback for unsupported SystemCapability  
        }  
      } catch (error) {  
        hilog.error(0x0001, TAG, `Failed to init customScan. Code: ${error?.code}, message: ${error?.message}`)  
      }  
    })()  
  }  

  aboutToDisappear(): void {  
    // 生命周期结束:停止与释放  
    this.userGrant = false  
    this.isFlashLightEnable = false  
    this.isSensorLight = false  
    try {  
      customScan.off?.('lightingFlash')  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to off lightingFlash. Code: ${error?.code}, message: ${error?.message}`)  
    }  
    this.customScanStop()  
    try {  
      customScan.release?.().catch((error: BusinessError) => {  
        hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error?.code}, message: ${error?.message}`)  
      })  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error?.code}, message: ${error?.message}`)  
    }  
  }  

  // 用户申请权限  
  async reqPermissionsFromUser(): Promise<number[]> {  
    hilog.info(0x0001, TAG, 'reqPermissionsFromUser start')  
    const context = (this.getUIContext().getHostContext() as common.UIAbilityContext)  
    const atManager = abilityAccessCtrl.createAtManager()  
    try {  
      const grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA'])  
      return grantStatus.authResults  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to requestPermissionsFromUser. Code: ${error?.code}, message: ${error?.message}`)  
      return []  
    }  
  }  

  // 用户申请相机权限  
  async requestCameraPermission() {  
    const grantStatus = await this.reqPermissionsFromUser()  
    for (let i = 0; i < grantStatus.length; i++) {  
      if (grantStatus[i] === 0) {  
        hilog.info(0x0001, TAG, 'Succeeded in getting permissions.')  
        this.userGrant = true  
        break  
      }  
    }  
  }  

  // 竖屏时获取屏幕尺寸,设置预览流全屏示例  
  setDisplay() {  
    try {  
      const displayClass = display.getDefaultDisplaySync()  
      this.displayHeight = this.getUIContext().px2vp(displayClass.height)  
      this.displayWidth = this.getUIContext().px2vp(displayClass.width)  
      const maxLen: number = Math.max(this.displayWidth, this.displayHeight)  
      const minLen: number = Math.min(this.displayWidth, this.displayHeight)  
      const RATIO: number = 16 / 9  
      this.cameraHeight = maxLen  
      this.cameraWidth = maxLen / RATIO  
      this.offsetX = (minLen - this.cameraWidth) / 2  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to getDefaultDisplaySync. Code: ${error?.code}, message: ${error?.message}`)  
    }  
  }  

  // toast显示扫码结果  
  async showScanResult(result: string = 'ok') {  
    try {  
      this.getUIContext().getPromptAction().showToast({  
        message: result,  
        duration: 3000  
      })  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to showToast. Code: ${error?.code}, message: ${error?.message}`)  
    }  
  }  

  initCamera() {  
    this.isShowBack = false  
    this.scanResult = []  
    const viewControl: customScan.ViewControl = {  
      width: this.cameraWidth,  
      height: this.cameraHeight,  
      surfaceId: this.surfaceId  
    }  
    try {  
        if (canIUse("SystemCapability.Multimedia.Scan.ScanBarcode")) {  
          customScan.start(viewControl)  
            .then((result) => {  
              hilog.info(0x0001, TAG, `result: ${JSON.stringify(result)}`)  
              if (result?.length) {  
                this.scanResult = result  
                this.isShowBack = true  
                // 事件回传  
                if (this.onScanResult) {  
                  console.log('onScanResult', result)  
                  const detail: ScanResultEventDetail = { results: result }  
                  this.onScanResult({ detail })  
                }  
                this.customScanStop()  
              }  
            })  
            .catch((error: BusinessError) => {  
              hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error?.code}, message: ${error?.message}`)  
            })  
        } else {  
          // Fallback for unsupported SystemCapability  
        }  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error?.code}, message: ${error?.message}`)  
    }  
  }  

  customScanStop() {  
    try {  
      if (this.canIUse) {  
        customScan.stop().catch((error: BusinessError) => {  
          hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error?.code}, message: ${error?.message}`)  
        })  
      } else {  
        // Fallback for unsupported SystemCapability  
      }  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error?.code}, message: ${error?.message}`)  
    }  
  }  

  public customGetZoom(): number {  
    let zoom = 1  
    try {  
      zoom = customScan.getZoom?.() ?? 1  
      hilog.info(0x0001, TAG, `Succeeded in getting zoom, zoom: ${zoom}`)  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to get zoom. Code: ${error?.code}, message: ${error?.message}`)  
    }  
    return zoom  
  }  

  public customSetZoom(pinchValue: number): void {  
    try {  
      customScan.setZoom?.(pinchValue)  
      hilog.info(0x0001, TAG, `Succeeded in setting zoom.`)  
    } catch (error) {  
      hilog.error(0x0001, TAG, `Failed to set zoom. Code: ${error?.code}, message: ${error?.message}`)  
    }  
  }  

  build() {  
    Stack() {  
      if (this.userGrant) {  
        Column() {  
          XComponent({  
            id: 'componentId',  
            type: XComponentType.SURFACE,  
            controller: this.mXComponentController  
          })  
            .onLoad(async () => {  
              hilog.info(0x0001, TAG, 'Succeeded in loading, onLoad is called.')  
              this.surfaceId = this.mXComponentController.getXComponentSurfaceId()  
              hilog.info(0x0001, TAG, `Succeeded in getting surfaceId: ${this.surfaceId}`)  
              this.initCamera()  
              // 闪光灯监听  
              try {  
                customScan.on?.('lightingFlash', (error: BusinessError, isLightingFlash: boolean) => {  
                  if (error) {  
                    hilog.error(0x0001, TAG,  
                      `Failed to on lightingFlash. Code: ${error?.code}, message: ${error?.message}`)  
                    return  
                  }  
                  if (isLightingFlash) {  
                    this.isFlashLightEnable = true  
                  } else {  
                    try {  
                      const status = customScan.getFlashLightStatus?.()  
                      if (!status) {  
                        this.isFlashLightEnable = false  
                      }  
                    } catch (error) {  
                      hilog.error(0x0001, TAG,  
                        `Failed to get flashLightStatus. Code: ${error?.code}, message: ${error?.message}`)  
                    }  
                  }  
                  this.isSensorLight = isLightingFlash  
                  if (this.onFlashLightChange) {  
                    // this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })  
                    console.warn('onFlashLightChange==')  

                  }  
                })  
              } catch (error) {  
                hilog.error(0x0001, TAG,  
                  `Failed to bind lightingFlash. Code: ${error?.code}, message: ${error?.message}`)  
              }  
            })  
            .width(this.cameraWidth)  
            .height(this.cameraHeight)  
            .position({ x: this.offsetX, y: this.offsetY })  
        }  
        .height('100%')  
        .width('100%')  
      }  

      // 操作区(简单控制:闪光灯、重新扫码、变焦)  
      Column() {  
        Row() {  
          Button('FlashLight')  
            .onClick(() => {  
              let lightStatus: boolean = false  
              try {  
                lightStatus = customScan.getFlashLightStatus?.() ?? false  
              } catch (error) {  
                hilog.error(0x0001, TAG,  
                  `Failed to get flashLightStatus. Code: ${error?.code}, message: ${error?.message}`)  
              }  
              if (lightStatus) {  
                try {  
                  customScan.closeFlashLight?.()  
                  setTimeout(() => {  
                    this.isFlashLightEnable = this.isSensorLight  
                    if (this.onFlashLightChange) {  
                      // this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })  
                      console.log('onFlashLightChange==')  
                    }  
                  }, 200)  
                } catch (error) {  
                  hilog.error(0x0001, TAG,  
                    `Failed to close flashLight. Code: ${error?.code}, message: ${error?.message}`)  
                }  
              } else {  
                try {  
                  customScan.openFlashLight();  
                  this.isFlashLightEnable = true  
                  if (this.onFlashLightChange) {  
                    // this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })  
                    console.log('onFlashLightChange==')  
                  }  
                } catch (error) {  
                  hilog.error(0x0001, TAG,  
                    `Failed to open flashLight. Code: ${error?.code}, message: ${error?.message}`)  
                }  
              }  
            })  
            .visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)  

          Button('Scan')  
            .onClick(() => {  
              this.initCamera()  
            })  
            .visibility(this.isShowBack ? Visibility.Visible : Visibility.None)  
        }  
        .margin({ top: 10, bottom: 10 })  

        Row() {  
          Button('缩放比例,当前比例:' + this.setZoomValue)  
            .onClick(() => {  
              if (!this.isShowBack) {  
                if (!this.zoomValue || this.zoomValue === this.setZoomValue) {  
                  this.setZoomValue = this.customGetZoom()  
                } else {  
                  this.zoomValue = this.zoomValue  
                  this.customSetZoom(this.zoomValue)  
                  setTimeout(() => {  
                    if (!this.isShowBack) {  
                      this.setZoomValue = this.customGetZoom()  
                    }  
                  }, 600)  
                }  
              }  
            })  
        }  
      }  
      .width('50%')  
      .height(180)  
    }  
    .width('100%')  
    .height('100%')  
    .onClick((event: ClickEvent) => {  
      if (this.isShowBack) {  
        return  
      }  
      // 设置点击对焦  
      const x1 = this.getUIContext().vp2px(event.displayY) / (this.displayHeight + 0.0)  
      const y1 = 1.0 - (this.getUIContext().vp2px(event.displayX) / (this.displayWidth + 0.0))  
      try {  
        customScan.setFocusPoint?.({ x: x1, y: y1 })  
        hilog.info(0x0001, TAG, `Succeeded to set focusPoint x1: ${x1}, y1: ${y1}`)  
      } catch (error) {  
        hilog.error(0x0001, TAG, `Failed to set focusPoint. Code: ${error?.code}, message: ${error?.message}`)  
      }  
      setTimeout(() => {  
        try {  
          customScan.resetFocus?.()  
        } catch (error) {  
          hilog.error(0x0001, TAG, `Failed to reset focus. Code: ${error?.code}, message: ${error?.message}`)  
        }  
      }, 200)  
    })  
    .gesture(PinchGesture({ fingers: 2 })  
      .onActionUpdate((event: GestureEvent) => {  
        if (event) {  
          this.scaleValue = event.scale  
        }  
      })  
      .onActionEnd((_: GestureEvent) => {  
        if (this.isShowBack) {  
          return  
        }  
        try {  
          const zoom = this.customGetZoom()  
          this.pinchValue = this.scaleValue * zoom  
          this.customSetZoom(this.pinchValue)  
          hilog.info(0x0001, TAG, 'Pinch end')  
        } catch (error) {  
          hilog.error(0x0001, TAG, `Failed to set zoom. Code: ${error?.code}, message: ${error?.message}`)  
        }  
      })  
    )  
  }  
}  
3 关注 分享
DCloud_CHB VK168 希语

要回复文章请先登录注册