华为HarmonyOS帮助应用快速构建强大的扫码能力 -- 2 自定义界面扫码能力

基本概念

自定义界面扫码能力提供了相机流控制接口,可根据自身需求自定义扫码界面,适用于对扫码界面有定制化需求的应用开发。

说明

通过自定义页面扫码可以实现应用内的扫码功能,为了应用更好的体验,推荐同时接入“扫码直达”服务,应用可以同时支持系统扫码入口(控制中心扫一扫)和应用内扫码两种方式跳转到指定服务页面。

场景介绍

自定义界面扫码能力提供扫码相机流控制接口,支持相机流的初始化、开启、暂停、释放、重新扫码功能;支持闪光灯的状态获取、开启、关闭;支持变焦比的获取和设置;支持设置相机焦点和连续自动对焦;支持对条形码、二维码、MULTIFUNCTIONAL CODE进行扫码识别(具体类型参见ScanType),并获得码类型、码值、码位置信息、相机预览流(YUV)。该能力可用于单码和多码的扫描识别。

开发者集成自定义界面扫码能力可以自行定义扫码的界面样式,请按照业务流程完成扫码接口调用实现实时扫码功能。建议开发者基于Sample Code做个性化修改。

扫码页面UX设计规范:

说明

YUV(相机预览流图像数据)适合于扫码和识物的综合识别场景,开发者需要自己控制相机流,普通扫码场景无需关注。

约束与限制

  • 需要请求相机的使用权限。
  • 需要开发者自行实现扫码的人机交互界面。例如:多码场景需要暂停相机流由用户选择一个码图进行识别。

业务流程

  1. 发起请求:用户向开发者的应用发起扫码请求,应用拉起已定义好的扫码界面。
  2. 申请授权:应用需要向用户申请相机权限授权。若未同意授权,则无法使用此功能。
  3. 启动自定义界面扫码:在扫码前必须调用init接口初始化自定义扫码界面,加载资源。相机流初始化结束后,调用start接口开始扫码。
  4. 自定义界面扫码相机操作:可以配置自定义界面扫码相机操作参数,调整相应功能,包括闪光灯、变焦、焦距、暂停、重启扫码等。例如:
    • 根据当前码图位置,比如当前码图太远或太近时,调用getZoom获取变焦比,setZoom接口设置变焦比,调整焦距以便于用户扫码。
    • 根据当前扫码的光线条件或根据on('lightingFlash')监听闪光灯开启时机,通过getFlashLightStatus接口先获取闪光灯状态,再调用openFlashLight/closeFlashLight接口控制闪光灯开启或关闭,以便于用户进行扫码。
    • 调用setFocusPoint设置对焦位置,resetFocus恢复默认对焦模式,以便于用户进行扫码。
    • 在应用处于前后台或其他特殊场景需要中断/重新进行扫码时,可调用stop或start接口来控制相机流达到暂停或重新扫码的目的。
  5. 自定义界面扫码:Scan Kit API在扫码完成后会返回扫码结果。同时根据开发者的需要,Scan Kit API会返回每帧相机预览流数据。如需不重启相机并重新触发一次扫码,可以在start接口的Callback异步回调中,调用rescan接口。完成扫码后,需调用release接口进行释放扫码资源的操作。
  6. 获取结果:解析码值结果跳转应用服务页。

接口说明

自定义界面扫码提供init、start、stop、release、getFlashLightStatus、openFlashLight、closeFlashLight、setZoom、getZoom、setFocusPoint、resetFocus、rescan、on('lightingFlash')、off('lightingFlash')接口,其中部分接口返回值有两种返回形式:Callback和Promise回调。Callback和Promise回调函数只是返回值方式不一样,功能相同。具体API说明详见接口文档

接口名

描述

init(options?: scanBarcode.ScanOptions): void

初始化自定义界面扫码,加载资源。无返回结果。

start(viewControl: ViewControl): Promise<Array<scanBarcode.ScanResult>>

启动扫码相机流。使用Promise异步回调获取扫码结果。

stop(): Promise<void>

暂停扫码相机流。使用Promise异步回调返回执行结果。

release(): Promise<void>

释放扫码相机流。使用Promise异步回调返回执行结果。

start(viewControl: ViewControl, callback: AsyncCallback<Array<scanBarcode.ScanResult>>, frameCallback?: AsyncCallback<ScanFrame>): void

启动扫码相机流。使用Callback异步回调返回扫码结果以及YUV图像数据。

getFlashLightStatus(): boolean

获取闪光灯状态。返回结果为布尔值,true为打开状态,false为关闭状态。

openFlashLight(): void

开启闪光灯。无返回结果。

closeFlashLight(): void

关闭闪光灯。无返回结果。

setZoom(zoomValue : number): void

设置变焦比。无返回结果。

getZoom(): number

获取当前的变焦比。

setFocusPoint(point: scanBarcode.Point): void

设置相机焦点。

resetFocus(): void

设置连续自动对焦模式。

rescan(): void

触发一次重新扫码。仅对start接口Callback异步回调有效,Promise异步回调无效。

stop(callback: AsyncCallback<void>): void

暂停扫码相机流。使用Callback异步回调返回执行结果。

release(callback: AsyncCallback<void>): void

释放扫码相机流。使用Callback异步回调返回执行结果。

on(type: 'lightingFlash', callback: AsyncCallback<boolean>): void

注册闪光灯打开时机回调,使用Callback异步回调返回闪光灯打开时机。

off(type: 'lightingFlash', callback?: AsyncCallback<boolean>): void

注销闪光灯打开时机回调,使用Callback异步回调返回注销结果。

开发步骤

自定义界面扫码接口支持自定义UI界面,识别相机流中的条形码,二维码以及MULTIFUNCTIONAL CODE,并返回码图的值、类型、码的位置信息(码图最小外接矩形左上角和右下角的坐标)以及相机预览流(YUV)。

以下示例为调用自定义界面扫码接口拉起相机流并返回扫码结果和相机预览流(YUV)。

  1. 在开发应用前,需要先申请相机相关权限,确保应用拥有访问相机的权限。在“module.json5”文件中配置相机权限,具体配置方式,请参见声明权限

    权限名

    说明

    授权方式

    ohos.permission.CAMERA

    允许应用使用相机扫码。

    user_grant

  2. 使用接口requestPermissionsFromUser请求用户授权。具体申请方式及校验方式,请参见向用户申请授权
  3. 导入自定义界面扫码接口以及相关接口模块,导入方法如下。
     
      
    1. import { scanCore, scanBarcode, customScan } from '@kit.ScanKit';
    2. // 导入功能涉及的权限申请、回调接口
    3. import { router, promptAction, display } from '@kit.ArkUI';
    4. import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';
    5. import { hilog } from '@kit.PerformanceAnalysisKit';
    6. import { common, abilityAccessCtrl } from '@kit.AbilityKit';
  4. 遵循业务流程完成自定义界面扫码功能。

    说明

    1. 在设置start接口的viewControl参数时,width和height与XComponent的宽高值相同,start接口会根据XComponent的宽高比例从相机的分辨率选择最优分辨率,如果比例与相机的分辨率比例相差过大会返回内部错误。当前支持的分辨率比例为16:9、4:3、1:1。竖屏场景下,XComponent的高度需要大于宽度,且高宽比在支持的分辨率比例中。横屏场景下,XComponent的宽度需要大于高度,且宽高比在支持的分辨率比例中。
    2. XComponent的宽高需根据使用场景计算适配。例如:在开发设备为折叠屏时,需按照折叠屏的展开态和折叠态分别计算XComponent的宽高,start接口会根据XComponent的宽高适配对应的相机分辨率。设备屏幕宽高可通过display.getDefaultDisplaySync方法获取(获取的为px单位,需要通过px2vp方法转为vp)。
    • 通过Promise方式回调,调用自定义界面扫码接口拉起相机流并返回扫码结果。
       
          
      1. const TAG: string = '[customScanPage]';
      2. @Entry
      3. @Component
      4. struct CustomScanPage {
      5. @State userGrant: boolean = false // 是否已申请相机权限
      6. @State surfaceId: string = '' // xComponent组件生成id
      7. @State isShowBack: boolean = false // 是否已经返回扫码结果
      8. @State isFlashLightEnable: boolean = false // 是否开启了闪光灯
      9. @State isSensorLight: boolean = false // 记录当前环境亮暗状态
      10. @State cameraHeight: number = 640 // 设置预览流高度,默认单位:vp
      11. @State cameraWidth: number = 360 // 设置预览流宽度,默认单位:vp
      12. @State offsetX: number = 0 // 设置预览流x轴方向偏移量,默认单位:vp
      13. @State offsetY: number = 0 // 设置预览流y轴方向偏移量,默认单位:vp
      14. @State zoomValue: number = 1 // 预览流缩放比例
      15. @State setZoomValue: number = 1 // 已设置的预览流缩放比例
      16. @State scaleValue: number = 1 // 屏幕缩放比
      17. @State pinchValue: number = 1 // 双指缩放比例
      18. @State displayHeight: number = 0 // 屏幕高度,单位vp
      19. @State displayWidth: number = 0 // 屏幕宽度,单位vp
      20. @State scanResult: Array<scanBarcode.ScanResult> = [] // 扫码结果
      21. private mXComponentController: XComponentController = new XComponentController()
      22. async onPageShow() {
      23. // 自定义启动第一步,用户申请权限
      24. await this.requestCameraPermission();
      25. // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false
      26. let options: scanBarcode.ScanOptions = {
      27. scanTypes: [scanCore.ScanType.ALL],
      28. enableMultiMode: true,
      29. enableAlbum: true
      30. }
      31. // 自定义启动第二步:设置预览流布局尺寸
      32. this.setDisplay();
      33. try {
      34. // 自定义启动第三步,初始化接口
      35. customScan.init(options);
      36. } catch (error) {
      37. hilog.error(0x0001, TAG, `Failed to init customScan. Code: ${error.code}, message: ${error.message}`);
      38. }
      39. }
      40. async onPageHide() {
      41. // 页面消失或隐藏时,停止并释放相机流
      42. this.userGrant = false;
      43. this.isFlashLightEnable = false;
      44. this.isSensorLight = false;
      45. try {
      46. customScan.off('lightingFlash');
      47. } catch (error) {
      48. hilog.error(0x0001, TAG, `Failed to off lightingFlash. Code: ${error.code}, message: ${error.message}`);
      49. }
      50. this.customScanStop();
      51. try {
      52. // 自定义相机流释放接口
      53. customScan.release().then(() => {
      54. hilog.info(0x0001, TAG, 'Succeeded in releasing customScan by promise.');
      55. }).catch((error: BusinessError) => {
      56. hilog.error(0x0001, TAG,
      57. `Failed to release customScan by promise. Code: ${error.code}, message: ${error.message}`);
      58. })
      59. } catch (error) {
      60. hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error.code}, message: ${error.message}`);
      61. }
      62. }
      63. // 用户申请权限
      64. async reqPermissionsFromUser(): Promise<number[]> {
      65. hilog.info(0x0001, TAG, 'reqPermissionsFromUser start');
      66. let context = getContext() as common.UIAbilityContext;
      67. let atManager = abilityAccessCtrl.createAtManager();
      68. let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
      69. return grantStatus.authResults;
      70. }
      71. // 用户申请相机权限
      72. async requestCameraPermission() {
      73. let grantStatus = await this.reqPermissionsFromUser();
      74. for (let i = 0; i < grantStatus.length; i++) {
      75. if (grantStatus[i] === 0) {
      76. // 用户授权,可以继续访问目标操作
      77. hilog.info(0x0001, TAG, 'Succeeded in getting permissions.');
      78. this.userGrant = true;
      79. }
      80. }
      81. }
      82. // 竖屏时获取屏幕尺寸,设置预览流全屏示例
      83. setDisplay() {
      84. try {
      85. // 默认竖屏
      86. let displayClass = display.getDefaultDisplaySync();
      87. this.displayHeight = px2vp(displayClass.height);
      88. this.displayWidth = px2vp(displayClass.width);
      89. let maxLen: number = Math.max(this.displayWidth, this.displayHeight);
      90. let minLen: number = Math.min(this.displayWidth, this.displayHeight);
      91. const RATIO: number = 16 / 9;
      92. this.cameraHeight = maxLen;
      93. this.cameraWidth = maxLen / RATIO;
      94. this.offsetX = (minLen - this.cameraWidth) / 2;
      95. } catch (error) {
      96. hilog.error(0x0001, TAG, `Failed to getDefaultDisplaySync. Code: ${error.code}, message: ${error.message}`);
      97. }
      98. }
      99. // toast显示扫码结果
      100. async showScanResult(result: scanBarcode.ScanResult) {
      101. // 使用toast显示出扫码结果
      102. promptAction.showToast({
      103. message: JSON.stringify(result),
      104. duration: 5000
      105. });
      106. }
      107. initCamera() {
      108. this.isShowBack = false;
      109. this.scanResult = [];
      110. let viewControl: customScan.ViewControl = {
      111. width: this.cameraWidth,
      112. height: this.cameraHeight,
      113. surfaceId: this.surfaceId
      114. };
      115. try {
      116. // 自定义启动第四步,请求扫码接口,通过Promise方式回调
      117. customScan.start(viewControl)
      118. .then(async (result: Array<scanBarcode.ScanResult>) => {
      119. hilog.info(0x0001, TAG, `result: ${JSON.stringify(result)}`);
      120. if (result.length) {
      121. // 解析码值结果跳转应用服务页
      122. this.scanResult = result;
      123. this.isShowBack = true;
      124. // 获取到扫描结果后暂停相机流
      125. this.customScanStop();
      126. }
      127. });
      128. } catch (error) {
      129. hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error.code}, message: ${error.message}`);
      130. }
      131. }
      132. customScanStop() {
      133. try {
      134. customScan.stop();
      135. } catch (error) {
      136. hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error.code}, message: ${error.message}`);
      137. }
      138. }
      139. // 自定义扫码界面的顶部返回按钮和扫码提示
      140. @Builder
      141. TopTool() {
      142. Column() {
      143. Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
      144. Text('返回')
      145. .onClick(async () => {
      146. router.back();
      147. })
      148. }.padding({ left: 24, right: 24, top: 40 })
      149. Column() {
      150. Text('扫描二维码/条形码')
      151. Text('对准二维码/条形码,即可自动扫描')
      152. }.margin({ left: 24, right: 24, top: 24 })
      153. }
      154. .height(146)
      155. .width('100%')
      156. }
      157. build() {
      158. Stack() {
      159. if (this.userGrant) {
      160. Column() {
      161. XComponent({
      162. id: 'componentId',
      163. type: XComponentType.SURFACE,
      164. controller: this.mXComponentController
      165. })
      166. .onLoad(async () => {
      167. hilog.info(0x0001, TAG, 'Succeeded in loading, onLoad is called.');
      168. // 获取XComponent组件的surfaceId
      169. this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
      170. hilog.info(0x0001, TAG, `Succeeded in getting surfaceId: ${this.surfaceId}`);
      171. this.initCamera();
      172. // 闪光灯监听接口
      173. customScan.on('lightingFlash', (error, isLightingFlash) => {
      174. if (error) {
      175. hilog.error(0x0001, TAG,
      176. `Failed to on lightingFlash. Code: ${error.code}, message: ${error.message}`);
      177. return;
      178. }
      179. if (isLightingFlash) {
      180. this.isFlashLightEnable = true;
      181. } else {
      182. try {
      183. if (!customScan.getFlashLightStatus()) {
      184. this.isFlashLightEnable = false;
      185. }
      186. } catch (error) {
      187. hilog.error(0x0001, TAG,
      188. `Failed to get FlashLightStatus. Code: ${error.code}, message: ${error.message}`);
      189. }
      190. }
      191. this.isSensorLight = isLightingFlash;
      192. });
      193. })
      194. .width(this.cameraWidth)
      195. .height(this.cameraHeight)
      196. .position({ x: this.offsetX, y: this.offsetY })
      197. }
      198. .height('100%')
      199. .width('100%')
      200. }
      201. Column() {
      202. this.TopTool()
      203. Column() {
      204. }
      205. .layoutWeight(1)
      206. .width('100%')
      207. Column() {
      208. Row() {
      209. // 闪光灯按钮,启动相机流后才能使用
      210. Button('FlashLight')
      211. .onClick(() => {
      212. let lightStatus: boolean = false;
      213. try {
      214. lightStatus = customScan.getFlashLightStatus();
      215. } catch (error) {
      216. hilog.error(0x0001, TAG,
      217. `Failed to get flashLightStatus. Code: ${error.code}, message: ${error.message}`);
      218. }
      219. // 根据当前闪光灯状态,选择打开或关闭闪关灯
      220. if (lightStatus) {
      221. try {
      222. customScan.closeFlashLight();
      223. setTimeout(() => {
      224. this.isFlashLightEnable = this.isSensorLight;
      225. }, 200);
      226. } catch (error) {
      227. hilog.error(0x0001, TAG,
      228. `Failed to close flashLight. Code: ${error.code}, message: ${error.message}`);
      229. }
      230. } else {
      231. try {
      232. customScan.openFlashLight();
      233. } catch (error) {
      234. hilog.error(0x0001, TAG,
      235. `Failed to open flashLight. Code: ${error.code}, message: ${error.message}`);
      236. }
      237. }
      238. })
      239. .visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)
      240. // 扫码成功后,点击按钮后重新扫码
      241. Button('Scan')
      242. .onClick(() => {
      243. // 点击按钮重启相机流,重新扫码
      244. this.initCamera();
      245. })
      246. .visibility(this.isShowBack ? Visibility.Visible : Visibility.None)
      247. }
      248. Row() {
      249. // 预览流设置缩放比例
      250. Button('缩放比例,当前比例:' + this.setZoomValue)
      251. .onClick(() => {
      252. // 设置相机缩放比例
      253. if (!this.isShowBack) {
      254. if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
      255. this.setZoomValue = this.customGetZoom();
      256. } else {
      257. this.zoomValue = this.zoomValue;
      258. this.customSetZoom(this.zoomValue);
      259. setTimeout(() => {
      260. if (!this.isShowBack) {
      261. this.setZoomValue = this.customGetZoom();
      262. }
      263. }, 1000);
      264. }
      265. }
      266. })
      267. }
      268. .margin({ top: 10, bottom: 10 })
      269. Row() {
      270. // 输入要设置的预览流缩放比例
      271. TextInput({ placeholder: '输入缩放倍数' })
      272. .type(InputType.Number)
      273. .borderWidth(1)
      274. .backgroundColor(Color.White)
      275. .onChange(value => {
      276. this.zoomValue = Number(value);
      277. })
      278. }
      279. }
      280. .width('50%')
      281. .height(180)
      282. }
      283. // 单码、多码扫描后,显示码图蓝点位置。点击toast码图信息
      284. ForEach(this.scanResult, (item: scanBarcode.ScanResult, index: number) => {
      285. if (item.scanCodeRect) {
      286. Image($rawfile('scan_selected2.svg'))
      287. .width(40)
      288. .height(40)
      289. .markAnchor({ x: 20, y: 20 })
      290. .position({
      291. x: (item.scanCodeRect.left + item?.scanCodeRect?.right) / 2 + this.offsetX,
      292. y: (item.scanCodeRect.top + item?.scanCodeRect?.bottom) / 2 + this.offsetY
      293. })
      294. .onClick(() => {
      295. this.showScanResult(item);
      296. })
      297. }
      298. })
      299. }
      300. // 建议相机流设置为全屏
      301. .width('100%')
      302. .height('100%')
      303. .onClick((event: ClickEvent) => {
      304. // 是否已扫描到结果
      305. if (this.isShowBack) {
      306. return;
      307. }
      308. // 点击屏幕位置,获取点击位置(x,y),设置相机焦点
      309. let x1 = vp2px(event.displayY) / (this.displayHeight + 0.0);
      310. let y1 = 1.0 - (vp2px(event.displayX) / (this.displayWidth + 0.0));
      311. try {
      312. customScan.setFocusPoint({ x: x1, y: y1 });
      313. hilog.info(0x0001, TAG, `Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}`);
      314. } catch (error) {
      315. hilog.error(0x0001, TAG, `Failed to set focusPoint. Code: ${error.code}, message: ${error.message}`);
      316. }
      317. hilog.info(0x0001, TAG, `Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}`);
      318. // 设置连续自动对焦模式
      319. setTimeout(() => {
      320. try {
      321. customScan.resetFocus();
      322. } catch (error) {
      323. hilog.error(0x0001, TAG, `Failed to reset Focus. Code: ${error.code}, message: ${error.message}`);
      324. }
      325. }, 200);
      326. }).gesture(PinchGesture({ fingers: 2 })
      327. .onActionStart((event: GestureEvent) => {
      328. hilog.info(0x0001, TAG, 'Pinch start');
      329. })
      330. .onActionUpdate((event: GestureEvent) => {
      331. if (event) {
      332. this.scaleValue = event.scale;
      333. }
      334. })
      335. .onActionEnd((event: GestureEvent) => {
      336. // 是否已扫描到结果
      337. if (this.isShowBack) {
      338. return;
      339. }
      340. // 获取双指缩放比例,设置变焦比
      341. try {
      342. let zoom = this.customGetZoom();
      343. this.pinchValue = this.scaleValue * zoom;
      344. this.customSetZoom(this.pinchValue);
      345. hilog.info(0x0001, TAG, 'Pinch end');
      346. } catch (error) {
      347. hilog.error(0x0001, TAG, `Failed to setZoom. Code: ${error.code}, message: ${error.message}`);
      348. }
      349. }))
      350. }
      351. public customGetZoom(): number {
      352. let zoom = 1;
      353. try {
      354. zoom = customScan.getZoom();
      355. hilog.info(0x0001, TAG, `Succeeded in getting Zoom, zoom: ${zoom}`);
      356. } catch (error) {
      357. hilog.error(0x0001, TAG, 'Failed to getZoom. Code: ${error.code}, message: ${error?.message}');
      358. }
      359. return zoom;
      360. }
      361. public customSetZoom(pinchValue: number): void {
      362. try {
      363. customScan.setZoom(pinchValue);
      364. hilog.info(0x0001, TAG, `Succeeded in setting Zoom.`);
      365. } catch (error) {
      366. hilog.error(0x0001, TAG, 'Failed to setZoom. Code: ${error.code}, message: ${error?.message}');
      367. }
      368. }
      369. }
    • 通过Callback方式回调,调用自定义界面扫码接口拉起相机流并返回扫码结果和相机预览流(YUV)。
       
          
      1. import { bundleManager, PermissionRequestResult, Permissions } from '@kit.AbilityKit';
      2. const TAG = '[YUV CPSample]';
      3. let context = getContext(this) as common.UIAbilityContext;
      4. // 用户申请权限
      5. export class PermissionsUtil {
      6. public static async checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
      7. let atManager = abilityAccessCtrl.createAtManager();
      8. let grantStatus: abilityAccessCtrl.GrantStatus = -1;
      9. // 获取应用程序的accessTokenID
      10. let tokenId: number = 0;
      11. let bundleInfo: bundleManager.BundleInfo =
      12. await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
      13. let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
      14. tokenId = appInfo.accessTokenId;
      15. // 校验应用是否被授予权限
      16. grantStatus = await atManager.checkAccessToken(tokenId, permission);
      17. return grantStatus;
      18. }
      19. // 申请相机权限
      20. public static async reqPermissionsFromUser(): Promise<number[]> {
      21. hilog.info(0x0001, TAG, 'Succeeded in getting permissions by promise.')
      22. let atManager = abilityAccessCtrl.createAtManager();
      23. let grantStatus: PermissionRequestResult = { permissions: [], authResults: [] }
      24. grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
      25. return grantStatus.authResults;
      26. }
      27. }
      28. @Extend(Column)
      29. function mainStyle() {
      30. .width('100%')
      31. .height('100%')
      32. .padding({
      33. top: 40
      34. })
      35. .justifyContent(FlexAlign.Center)
      36. }
      37. @Entry
      38. @Component
      39. struct YUVScan {
      40. @State userGrant: boolean = false // 是否已申请相机权限
      41. @State surfaceId: string = '' // xComponent组件生成id
      42. @State cameraHeight: number = 640 // 设置预览流高度,默认单位:vp
      43. @State cameraWidth: number = 360 // 设置预览流宽度,默认单位:vp
      44. @State zoomValue: number = 1 // 预览流缩放比例
      45. @State setZoomValue: number = 1 // 已设置的预览流缩放比例
      46. @State isReleaseCamera: boolean = false // 是否已释放相机流
      47. @State scanWidth: number = 384 // xComponent宽度,默认设置384,单位vp
      48. @State scanHeight: number = 682 // xComponent高度,默认设置682,单位vp
      49. @State scanBottom: number = 220
      50. @State offsetX: number = 0 // xComponent位置x轴偏移量,单位vp
      51. @State offsetY: number = 0 // xComponent位置y轴偏移量,单位vp
      52. @State scanCodeRect: Array<scanBarcode.ScanCodeRect> = [] // 扫码结果码图位置
      53. @State scanFlag: boolean = false // 是否已经扫码到结果
      54. @State scanFrameResult: string = ''
      55. @State scaleValue: number = 1 // 屏幕缩放比
      56. @State pinchValue: number = 1 // 双指缩放比例
      57. @State displayHeight: number = 0 // 屏幕高度,单位vp
      58. @State displayWidth: number = 0 // 屏幕宽度,单位vp
      59. private mXComponentController: XComponentController = new XComponentController()
      60. private viewControl: customScan.ViewControl = { width: 1920, height: 1080, surfaceId: this.surfaceId }
      61. options: scanBarcode.ScanOptions = {
      62. // 扫码类型,可选参数
      63. scanTypes: [scanCore.ScanType.ALL],
      64. // 是否开启多码识别,可选参数
      65. enableMultiMode: true,
      66. // 是否开启相册扫码,可选参数
      67. enableAlbum: true,
      68. }
      69. // 返回自定义扫描结果的回调
      70. private callback: AsyncCallback<scanBarcode.ScanResult[]> =
      71. async (error: BusinessError, result: scanBarcode.ScanResult[]) => {
      72. if (error && error.code) {
      73. hilog.error(0x0001, TAG,
      74. `Failed to get ScanResult by callback. Code: ${error.code}, message: ${error.message}`);
      75. return;
      76. }
      77. // 解析码值结果跳转应用服务页
      78. hilog.info(0x0001, TAG, `Succeeded in getting ScanResult by callback, result: ${JSON.stringify(result)}`);
      79. }
      80. // 返回相机帧的回调
      81. private frameCallback: AsyncCallback<customScan.ScanFrame> =
      82. async (error: BusinessError, frameResult: customScan.ScanFrame) => {
      83. if (error) {
      84. hilog.error(0x0001, TAG, `Failed to get ScanFrame by callback. Code: ${error.code}, message: ${error.message}`);
      85. return;
      86. }
      87. // byteBuffer相机YUV图像数组
      88. hilog.info(0x0001, TAG,
      89. `Succeeded in getting ScanFrame.byteBuffer.byteLength: ${frameResult.byteBuffer.byteLength}`)
      90. hilog.info(0x0001, TAG, `Succeeded in getting ScanFrame.width: ${frameResult.width}`)
      91. hilog.info(0x0001, TAG, `Succeeded in getting ScanFrame.height: ${frameResult.height}`)
      92. this.scanFrameResult = JSON.stringify(frameResult.scanCodeRects);
      93. if (frameResult && frameResult.scanCodeRects && frameResult.scanCodeRects.length > 0 && !this.scanFlag) {
      94. if (frameResult.scanCodeRects[0]) {
      95. this.stopCamera();
      96. this.scanCodeRect = [];
      97. this.scanFlag = true;
      98. // 码图位置信息转换
      99. this.changeToXComponent(frameResult);
      100. } else {
      101. this.scanFlag = false;
      102. }
      103. }
      104. }
      105. // frameCallback横向码图位置信息转换为预览流xComponent对应码图位置信息
      106. changeToXComponent(frameResult: customScan.ScanFrame) {
      107. if (frameResult && frameResult.scanCodeRects) {
      108. let frameHeight = frameResult.height;
      109. let ratio = this.scanWidth / frameHeight;
      110. frameResult.scanCodeRects.forEach((item) => {
      111. this.scanCodeRect.push({
      112. left: this.toFixedNumber((frameHeight - item.bottom) * ratio),
      113. top: this.toFixedNumber(item.left * ratio),
      114. right: this.toFixedNumber((frameHeight - item.top) * ratio),
      115. bottom: this.toFixedNumber(item.right * ratio)
      116. });
      117. });
      118. this.scanFrameResult = JSON.stringify(this.scanCodeRect);
      119. }
      120. }
      121. toFixedNumber(no: number): number {
      122. return Number((no).toFixed(1));
      123. }
      124. async onPageShow() {
      125. // 自定义启动第一步,用户申请权限
      126. const permissions: Array<Permissions> = ['ohos.permission.CAMERA'];
      127. // 自定义启动第二步:设置预览流布局尺寸
      128. this.setDisplay();
      129. let grantStatus = await PermissionsUtil.checkAccessToken(permissions[0]);
      130. if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      131. // 已经授权,可以继续访问目标操作
      132. this.userGrant = true;
      133. if (this.surfaceId) {
      134. // 自定义启动第三步,初始化接口
      135. this.initCamera();
      136. }
      137. } else {
      138. // 申请相机权限
      139. this.requestCameraPermission();
      140. }
      141. }
      142. async onPageHide() {
      143. this.releaseCamera();
      144. }
      145. // 用户申请权限
      146. async requestCameraPermission() {
      147. let grantStatus = await PermissionsUtil.reqPermissionsFromUser()
      148. let length: number = grantStatus.length;
      149. for (let i = 0; i < length; i++) {
      150. if (grantStatus[i] === 0) {
      151. // 用户授权,可以继续访问目标操作
      152. this.userGrant = true;
      153. } else {
      154. // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
      155. this.userGrant = false;
      156. }
      157. }
      158. }
      159. // 竖屏时获取屏幕尺寸,设置预览流全屏示例
      160. setDisplay() {
      161. try {
      162. // 以手机为例计算宽高
      163. let displayClass = display.getDefaultDisplaySync();
      164. this.displayHeight = px2vp(displayClass.height);
      165. this.displayWidth = px2vp(displayClass.width);
      166. if (displayClass !== null) {
      167. this.scanWidth = px2vp(displayClass.width);
      168. this.scanHeight = Math.round(this.scanWidth * this.viewControl.width / this.viewControl.height);
      169. this.scanBottom = Math.max(220, px2vp(displayClass.height) - this.scanHeight);
      170. this.offsetX = 0;
      171. this.offsetY = 0;
      172. }
      173. } catch (error) {
      174. hilog.error(0x0001, TAG, `Failed to getDefaultDisplaySync. Code: ${error.code}, message: ${error.message}`);
      175. }
      176. }
      177. // 初始化相机流
      178. async initCamera() {
      179. this.isReleaseCamera = false;
      180. try {
      181. // 自定义启动第三步,初始化接口
      182. customScan.init(this.options);
      183. hilog.info(0x0001, TAG, 'Succeeded in initting customScan with options.');
      184. } catch (error) {
      185. hilog.error(0x0001, TAG, `Failed to init customScan. Code: ${error.code}, message: ${error.message}`);
      186. }
      187. this.scanCodeRect = [];
      188. this.scanFlag = false;
      189. try {
      190. // 自定义启动第四步,请求扫码接口
      191. customScan.start(this.viewControl, this.callback, this.frameCallback);
      192. } catch (error) {
      193. hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error.code}, message: ${error.message}`);
      194. }
      195. }
      196. // 暂停相机流
      197. async stopCamera() {
      198. if (!this.isReleaseCamera) {
      199. try {
      200. customScan.stop();
      201. } catch (error) {
      202. hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error.code}, message: ${error.message}`);
      203. }
      204. }
      205. }
      206. // 释放相机流
      207. async releaseCamera() {
      208. if (!this.isReleaseCamera) {
      209. await this.stopCamera();
      210. try {
      211. await customScan.release();
      212. } catch (error) {
      213. hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error.code}, message: ${error.message}`);
      214. }
      215. this.isReleaseCamera = true;
      216. }
      217. }
      218. build() {
      219. Stack() {
      220. // 相机预览流XComponent
      221. if (this.userGrant) {
      222. Column() {
      223. XComponent({
      224. id: 'componentId',
      225. type: XComponentType.SURFACE,
      226. controller: this.mXComponentController
      227. })
      228. .onLoad(() => {
      229. hilog.info(0x0001, TAG, 'Succeeded in loading, onLoad is called.');
      230. this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
      231. hilog.info(0x0001, TAG, `Succeeded in getting surfaceId is ${this.surfaceId}`);
      232. this.viewControl = { width: this.scanWidth, height: this.scanHeight, surfaceId: this.surfaceId };
      233. // 启动相机进行扫码
      234. this.initCamera();
      235. })
      236. .height(this.scanHeight)
      237. .width(this.scanWidth)
      238. .position({ x: 0, y: 0 })
      239. }
      240. .height('100%')
      241. .width('100%')
      242. .position({ x: this.offsetX, y: this.offsetY })
      243. }
      244. Column() {
      245. Column() {
      246. }
      247. .layoutWeight(1)
      248. .width('100%')
      249. Column() {
      250. Row() {
      251. // 闪光灯按钮,启动相机流后才能使用
      252. Button('FlashLight')
      253. .onClick(() => {
      254. let lightStatus: boolean = false;
      255. try {
      256. lightStatus = customScan.getFlashLightStatus();
      257. } catch (error) {
      258. hilog.error(0x0001, TAG,
      259. `Failed to get flashLightStatus. Code: ${error.code}, message: ${error.message}`);
      260. }
      261. // 根据当前闪光灯状态,选择打开或关闭闪关灯
      262. if (lightStatus) {
      263. try {
      264. customScan.closeFlashLight();
      265. } catch (error) {
      266. hilog.error(0x0001, TAG,
      267. `Failed to close flashLight. Code: ${error.code}, message: ${error.message}`);
      268. }
      269. } else {
      270. try {
      271. customScan.openFlashLight();
      272. } catch (error) {
      273. hilog.error(0x0001, TAG,
      274. `Failed to open flashLight. Code: ${error.code}, message: ${error.message}`);
      275. }
      276. }
      277. })
      278. .visibility(this.scanFlag ? Visibility.None : Visibility.Visible)
      279. }
      280. Row() {
      281. // 预览流设置缩放比例
      282. Button('缩放比例,当前比例:' + this.setZoomValue)
      283. .width(200)
      284. .alignSelf(ItemAlign.Center)
      285. .onClick(() => {
      286. // 设置相机缩放比例
      287. if (!this.scanFlag) {
      288. if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
      289. this.setZoomValue = this.customGetZoom();
      290. } else {
      291. this.zoomValue = this.zoomValue;
      292. this.customSetZoom(this.zoomValue);
      293. setTimeout(() => {
      294. if (!this.scanFlag) {
      295. this.setZoomValue = this.customGetZoom();
      296. }
      297. }, 1000);
      298. }
      299. }
      300. })
      301. }
      302. .margin({ top: 10, bottom: 10 })
      303. .visibility(this.scanFlag ? Visibility.None : Visibility.Visible)
      304. Row() {
      305. // 输入要设置的预览流缩放比例
      306. TextInput({ placeholder: '输入缩放倍数' })
      307. .width(200)
      308. .type(InputType.Number)
      309. .borderWidth(1)
      310. .backgroundColor(Color.White)
      311. .onChange(value => {
      312. this.zoomValue = Number(value);
      313. })
      314. }
      315. .visibility(this.scanFlag ? Visibility.None : Visibility.Visible)
      316. Text(this.scanFlag ? '继续扫码' : '扫码中')
      317. .height(30)
      318. .fontSize(16)
      319. .fontColor(Color.White)
      320. .onClick(() => {
      321. if (this.scanFlag) {
      322. this.scanFrameResult = '';
      323. this.initCamera();
      324. }
      325. })
      326. Text('扫码结果:' + this.scanFrameResult).fontColor(Color.White).fontSize(12)
      327. }
      328. .width('100%')
      329. .height(this.scanBottom)
      330. .backgroundColor(Color.Black)
      331. }
      332. .mainStyle()
      333. Image($rawfile('scan_back.svg'))
      334. .width(20)
      335. .height(20)
      336. .position({
      337. x: 40,
      338. y: 40
      339. })
      340. .onClick(() => {
      341. router.back();
      342. })
      343. // 实时扫码码图中心点位置
      344. if (this.scanFlag && this.scanCodeRect.length > 0) {
      345. ForEach(this.scanCodeRect, (item: scanBarcode.ScanCodeRect, index: number) => {
      346. Image($rawfile('scan_selected2.svg'))
      347. .width(40)
      348. .height(40)
      349. .markAnchor({ x: 20, y: 20 })
      350. .position({
      351. x: (item.left + item.right) / 2 + this.offsetX,
      352. y: (item.top + item.bottom) / 2 + this.offsetY
      353. })
      354. })
      355. }
      356. }
      357. .width('100%')
      358. .height('100%')
      359. .backgroundColor(this.userGrant ? Color.Transparent : Color.Black)
      360. .onClick((event: ClickEvent) => {
      361. // 是否已扫描到结果
      362. if (this.scanFlag) {
      363. return;
      364. }
      365. // 点击屏幕位置,获取点击位置(x,y),设置相机焦点
      366. let x1 = vp2px(event.displayY) / (this.displayHeight + 0.0);
      367. let y1 = 1.0 - (vp2px(event.displayX) / (this.displayWidth + 0.0));
      368. try {
      369. customScan.setFocusPoint({ x: x1, y: y1 });
      370. hilog.info(0x0001, TAG, `Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}`);
      371. } catch (error) {
      372. hilog.error(0x0001, TAG, `Failed to set focusPoint. Code: ${error.code}, message: ${error.message}`);
      373. }
      374. setTimeout(() => {
      375. try {
      376. customScan.resetFocus();
      377. } catch (error) {
      378. hilog.error(0x0001, TAG, `Failed to reset Focus. Code: ${error.code}, message: ${error.message}`);
      379. }
      380. }, 200);
      381. })
      382. .gesture(PinchGesture({ fingers: 2 })
      383. .onActionStart((event: GestureEvent) => {
      384. hilog.info(0x0001, TAG, 'Pinch start');
      385. })
      386. .onActionUpdate((event: GestureEvent) => {
      387. if (event) {
      388. this.scaleValue = event.scale;
      389. }
      390. })
      391. .onActionEnd((event: GestureEvent) => {
      392. // 是否已扫描到结果
      393. if (this.scanFlag) {
      394. return;
      395. }
      396. // 获取双指缩放比例,设置变焦比
      397. try {
      398. let zoom = this.customGetZoom();
      399. this.pinchValue = this.scaleValue * zoom;
      400. this.customSetZoom(this.pinchValue);
      401. hilog.info(0x0001, TAG, 'Pinch end');
      402. } catch (error) {
      403. hilog.error(0x0001, TAG, `Failed to setZoom. Code: ${error.code}, message: ${error.message}`);
      404. }
      405. }))
      406. }
      407. public customGetZoom(): number {
      408. let zoom = 1;
      409. try {
      410. zoom = customScan.getZoom();
      411. hilog.info(0x0001, TAG, `Succeeded in getting Zoom, zoom: ${zoom}`);
      412. } catch (error) {
      413. hilog.error(0x0001, TAG, 'Failed to getZoom. Code: ${error.code}, message: ${error?.message}');
      414. }
      415. return zoom;
      416. }
      417. public customSetZoom(pinchValue: number): void {
      418. try {
      419. customScan.setZoom(pinchValue);
      420. hilog.info(0x0001, TAG, `Succeeded in setting Zoom.`);
      421. } catch (error) {
      422. hilog.error(0x0001, TAG, 'Failed to setZoom. Code: ${error.code}, message: ${error?.message}');
      423. }
      424. }
      425. }
  5. 通过scanCodeRect数据可确定码图中心点的位置,使用说明如下。
    • scanCodeRect的四个点坐标如下,可根据坐标点绘制码图外围矩形框。
      • 左上角(x, y):(left, top)
      • 右上角(x, y):(right, top)
      • 左下角(x, y):(left, bottom)
      • 右下角(x, y):(right, bottom)
    • 由于码图中心点坐标需和xComponent的坐标保持一致,如果xComponent的x轴和y轴存在偏移,则码图位置需做相应的偏移。例如:x轴偏移量为:offsetX;y轴偏移量为:offsetY,中心点坐标最终转换为:
      • x = (left + right) / 2 + offsetX
      • y = (top + bottom) / 2 + offsetY

模拟器开发

暂不支持模拟器使用,调用会返回错误信息“Emulator is not supported.”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青瓷代码世界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值