有些应用场景,需要使用自定义扫码的界面,添加自定义的布局和功能,相比 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.uts 和 scan.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}`)
}
})
)
}
}

