1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { display, promptAction, window } from '@kit.ArkUI'; 17import { customScan, detectBarcode, scanBarcode, scanCore } from '@kit.ScanKit'; 18import { BusinessError } from '@kit.BasicServicesKit'; 19import { photoAccessHelper } from '@kit.MediaLibraryKit'; 20 21import { logger } from '../common/util/Logger'; 22import CommonConstants from '../common/constants/CommonConstants'; 23import PermissionModel from '../model/PermissionModel'; 24import WindowModel from '../model/WindowModel'; 25import { ScanSize } from '../model/ScanSize'; 26 27@Observed 28export default class CustomScanViewModel { 29 /** 30 * 单例模型私有化构造函数,使用getInstance静态方法获得单例 31 */ 32 private constructor() { 33 // 初始化窗口管理model 34 const windowStage: window.WindowStage | undefined = AppStorage.get('windowStage'); 35 if (windowStage) { 36 this.windowModel.setWindowStage(windowStage); 37 } 38 39 // 初始化相机流尺寸 40 this.updateCameraCompSize(); 41 } 42 43 /** 44 * CustomScanViewModel 单例 45 */ 46 private static instance?: CustomScanViewModel; 47 48 /** 49 * 获取CustomScanViewModel单例实例 50 * @returns {CustomScanViewModel} CustomScanViewModel 51 */ 52 static getInstance(): CustomScanViewModel { 53 if (!CustomScanViewModel.instance) { 54 CustomScanViewModel.instance = new CustomScanViewModel(); 55 } 56 57 return CustomScanViewModel.instance; 58 } 59 60 /** 61 * 是否开启扫描动画 62 */ 63 public isScanLine: Boolean = false; 64 /** 65 * 是否扫描成功 66 */ 67 private isScannedRaw: boolean = false; 68 get isScanned () { 69 return this.isScannedRaw; 70 } 71 set isScanned (val: boolean) { 72 this.isScannedRaw = val; 73 } 74 /** 75 * 扫描结果 76 */ 77 public scanResult: ScanResults = new ScanResults(); 78 /** 79 * 扫描结果内容弹窗id 80 */ 81 public scanResultDialogId?: number; 82 /** 83 * PermissionModel 单例 84 */ 85 private permissionModel: PermissionModel = PermissionModel.getInstance(); 86 /** 87 * WindowModel 单例 88 */ 89 private windowModel: WindowModel = WindowModel.getInstance(); 90 91 private scanSize: ScanSize = ScanSize.getInstance(); 92 /** 93 * 自定义扫码初始化配置参数 94 */ 95 private customScanInitOptions: scanBarcode.ScanOptions = { 96 scanTypes: [scanCore.ScanType.QR_CODE], 97 enableMultiMode: true, 98 enableAlbum: true 99 } 100 /** 101 * 当前屏幕折叠态(仅折叠屏设备下有效) 102 */ 103 public curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN; 104 /** 105 * 相机流展示组件(XComponent)的surfaceId 106 */ 107 public surfaceId: string = ''; 108 /** 109 * 相机流展示组件(XComponent)的宽 110 */ 111 public cameraCompWidth: number = 0; 112 /** 113 * 相机流展示组件(XComponent)的高 114 */ 115 public cameraCompHeight: number = 0; 116 /** 117 * 相机流展示组件(XComponent)的水平偏移位置 118 */ 119 public cameraCompOffsetX: number = 0; 120 /** 121 * 相机流展示组件(XComponent)的竖直偏移位置 122 */ 123 public cameraCompOffsetY: number = 0; 124 /** 125 * 相机流展示组件内容尺寸修改回调函数 126 */ 127 public cameraCompSizeUpdateCb: Function = (): void => { 128 }; 129 /** 130 * 相机闪光灯状态更新回调函数 131 */ 132 public cameraLightUpdateCb: Function = (): void => { 133 }; 134 135 /** 136 * 检测是否有相机权限,未授权尝试申请授权 137 * @returns {Promise<boolean>} 相机权限/授权结果 138 */ 139 async reqCameraPermission(): Promise<boolean> { 140 const reqPermissionName = CommonConstants.PERMISSION_CAMERA; 141 // 优先检测是否已授权 142 let isGranted = await this.permissionModel.checkPermission(reqPermissionName); 143 if (isGranted) { 144 return true; 145 } 146 // 没有授权申请授权 147 isGranted = await this.permissionModel.requestPermission(reqPermissionName); 148 return isGranted; 149 } 150 151 /** 152 * 当前主窗口是否开启沉浸模式 153 * @param {boolean} enable 是否开启 154 * @returns {void} 155 */ 156 setMainWindowImmersive(enable: boolean): void { 157 this.windowModel.setMainWindowImmersive(enable); 158 } 159 160 /** 161 * 更新相机流展示组件(XComponent)的尺寸 162 * @returns {void} 163 */ 164 async updateCameraCompSize(): Promise<void> { 165 // 通过窗口属性修改组件宽高 166 let windowSize: window.Size | null = this.scanSize.setWindowSize(); 167 if (windowSize) { 168 this.scanSize.setScanXComponentSize(true, windowSize) 169 170 this.cameraCompWidth = this.scanSize.xComponentSize.width; 171 this.cameraCompHeight = this.scanSize.xComponentSize.height 172 this.cameraCompOffsetX = this.scanSize.xComponentSize.offsetX; 173 this.cameraCompOffsetY = this.scanSize.xComponentSize.offsetY; 174 } 175 176 logger.debug(`updateCameraCompSize: width=${this.cameraCompWidth} height=${this.cameraCompHeight}`); 177 } 178 179 /** 180 * 注册屏幕状态监听 181 * @returns {void} 182 */ 183 regDisplayListener(): void { 184 if (display.isFoldable()) { 185 // 监听折叠屏状态变更,更新折叠态,修改窗口显示方向 186 display.on('foldStatusChange', async (curFoldStatus: display.FoldStatus) => { 187 // 无视FOLD_STATUS_UNKNOWN状态 188 if (curFoldStatus === display.FoldStatus.FOLD_STATUS_UNKNOWN) { 189 return; 190 } 191 // FOLD_STATUS_HALF_FOLDED状态当作FOLD_STATUS_EXPANDED一致处理 192 if (curFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) { 193 curFoldStatus = display.FoldStatus.FOLD_STATUS_EXPANDED; 194 } 195 // 同一个状态重复触发不做处理 196 if (this.curFoldStatus === curFoldStatus) { 197 return; 198 } 199 200 // 缓存当前折叠状态 201 this.curFoldStatus = curFoldStatus; 202 203 // 当前没有相机流资源,只更新相机流宽高设置 204 if (!this.surfaceId) { 205 this.updateCameraCompSize(); 206 return; 207 } 208 209 // 关闭闪光灯 210 this.tryCloseFlashLight(); 211 setTimeout(() => { 212 // 重新启动扫码 213 this.restartCustomScan(); 214 }, 10) 215 }) 216 } 217 } 218 219 /** 220 * 注册相机流展示组件内容尺寸修改回调函数 221 * @param {Function} callback 相机流展示组件内容尺寸修改回调函数 222 * @returns {void} 223 */ 224 regXCompSizeUpdateListener(callback: Function): void { 225 this.cameraCompSizeUpdateCb = callback; 226 } 227 228 /** 229 * 注册相机闪光灯状态更新回调函数 230 * @param {Function} callback 相机闪光灯状态更新回调函数 231 * @returns {void} 232 */ 233 regCameraLightUpdateListener(callback: Function): void { 234 this.cameraLightUpdateCb = callback; 235 } 236 237 /** 238 * 更新相机闪光灯状态 239 * @returns {void} 240 */ 241 updateFlashLightStatus(): void { 242 // 根据当前闪光灯状态,选择打开或关闭闪关灯 243 try { 244 let isCameraLightOpen: boolean = false; 245 if (customScan.getFlashLightStatus()) { 246 customScan.closeFlashLight(); 247 isCameraLightOpen = false; 248 } else { 249 customScan.openFlashLight(); 250 isCameraLightOpen = true; 251 } 252 253 this.cameraLightUpdateCb(isCameraLightOpen); 254 } catch (error) { 255 logger.error('flashLight control failed, error: ' + JSON.stringify(error)); 256 } 257 } 258 259 /** 260 * 尝试把开启的闪光灯关闭 261 * @returns {void} 262 */ 263 tryCloseFlashLight() { 264 try { 265 // 闪光灯标记移除 266 this.cameraLightUpdateCb(false); 267 // 如果闪光灯开启,则关闭 268 if (customScan.getFlashLightStatus()) { 269 customScan.closeFlashLight(); 270 } 271 } catch (error) { 272 logger.error('flashLight try close failed, error: ' + JSON.stringify(error)); 273 } 274 } 275 276 /** 277 * 自定义扫码数据初始化 278 * @returns {void} 279 */ 280 initScanData() { 281 this.isScanLine = false; 282 this.isScanned = false; 283 this.scanResult = {} as ScanResults; 284 } 285 286 /** 287 * 初始化自定义扫码 288 * @returns {void} 289 */ 290 initCustomScan(): void { 291 logger.info('initCustomScan'); 292 try { 293 this.initScanData() 294 customScan.init(this.customScanInitOptions); 295 this.startCustomScan(); 296 } catch (error) { 297 logger.error('init fail, error: ' + JSON.stringify(error)); 298 } 299 } 300 301 /** 302 * 启动自定义扫码 303 * @returns {void} 304 */ 305 startCustomScan(): void { 306 logger.info('startCustomScan'); 307 try { 308 const viewControl: customScan.ViewControl = { 309 width: this.cameraCompWidth, 310 height: this.cameraCompHeight, 311 surfaceId: this.surfaceId 312 }; 313 customScan.start(viewControl).then((result) => { 314 // TODO:知识点 请求扫码结果,通过Promise触发回调 315 this.customScanCallback(result); 316 }).catch((error: BusinessError) => { 317 logger.error('customScan start failed error: ' + JSON.stringify(error)); 318 }) 319 this.isScanLine = true; 320 } catch (error) { 321 logger.error('startCustomScan failed error: ' + JSON.stringify(error)); 322 } 323 } 324 325 /** 326 * 重新触发一次扫码(仅能使用在customScan.start的异步回调中) 327 * @returns {void} 328 */ 329 reCustomScan(): void { 330 try { 331 customScan.rescan(); 332 } catch (error) { 333 logger.error('reCustomScan failed error: ' + JSON.stringify(error)); 334 } 335 } 336 337 /** 338 * 停止自定义扫码 339 * @returns {void} 340 */ 341 stopCustomScan(): void { 342 // 关闭相机闪光灯 343 this.tryCloseFlashLight(); 344 345 // 关闭自定义扫码 346 try { 347 this.isScanLine = false; 348 customScan.stop().then(() => { 349 }).catch((error: BusinessError) => { 350 logger.error('customScan stop failed error: ' + JSON.stringify(error)); 351 }) 352 } catch (error) { 353 logger.error('stopCustomScan: stop error: ' + JSON.stringify(error)); 354 } 355 } 356 357 /** 358 * 释放自定义扫码资源 359 * @returns {Promise<void>} 360 */ 361 async releaseCustomScan(): Promise<void> { 362 logger.info('releaseCustomScan'); 363 try { 364 await customScan.release(); 365 } catch (error) { 366 logger.error('Catch: release error: ' + JSON.stringify(error)); 367 } 368 } 369 370 /** 371 * 重新启动扫码 372 * @returns {void} 373 */ 374 async restartCustomScan(): Promise<void> { 375 logger.info('restartCustomScan'); 376 // 关闭存在的扫描结果对话框 377 this.closeScanResult(); 378 // 根据窗口尺寸调整展示组件尺寸 379 await this.updateCameraCompSize(); 380 // 调整相机surface尺寸 381 this.cameraCompSizeUpdateCb(this.cameraCompWidth, 382 this.cameraCompHeight, 383 this.cameraCompOffsetX, 384 this.cameraCompOffsetY); 385 // 释放扫码资源 386 await this.releaseCustomScan(); 387 // 初始化相机资源并启动 388 this.initCustomScan(); 389 } 390 391 /** 392 * 扫码结果回调 393 * @param {scanBarcode.ScanResult[]} result 扫码结果数据 394 * @returns {void} 395 */ 396 customScanCallback(result: scanBarcode.ScanResult[]): void { 397 if (!this.isScanned) { 398 this.scanResult.code = 0; 399 this.scanResult.data = result || []; 400 let resultLength: number = result ? result.length : 0; 401 if (resultLength) { 402 // 停止扫描 403 this.stopCustomScan(); 404 // 标记扫描状态,触发UI刷新 405 this.isScanned = true; 406 this.scanResult.size = resultLength; 407 } else { 408 // 重新扫码 409 this.reCustomScan() 410 } 411 } 412 } 413 414 /** 415 * 关闭扫码结果 416 * @returns {void} 417 */ 418 closeScanResult(): void { 419 logger.info('closeScanResult'); 420 if (this.scanResultDialogId) { 421 promptAction.closeCustomDialog(this.scanResultDialogId); 422 this.scanResultDialogId = undefined; 423 } 424 } 425 426 /** 427 * 打开系统相册,选择照片进行二维码识别 428 * @returns {string} 429 */ 430 async detectFromPhotoPicker(): Promise<string> { 431 const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); 432 photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; 433 photoSelectOptions.maxSelectNumber = 1; 434 const photoViewPicker = new photoAccessHelper.PhotoViewPicker(); 435 const photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions); 436 const uris: string[] = photoSelectResult.photoUris; 437 if (uris.length === 0) { 438 return ''; 439 } 440 441 // 识别结果 442 let retVal = CommonConstants.DETECT_NO_RESULT; 443 const inputImage: detectBarcode.InputImage = { uri: uris[0] }; 444 try { 445 // 定义识码参数options 446 let options: scanBarcode.ScanOptions = { 447 scanTypes: [scanCore.ScanType.QR_CODE], 448 enableMultiMode: true, 449 enableAlbum: true, 450 } 451 // 调用图片识码接口 452 const decodeResult: scanBarcode.ScanResult[] = await detectBarcode.decode(inputImage, options); 453 if (decodeResult.length > 0) { 454 retVal = decodeResult[0].originalValue; 455 } 456 logger.error('[customscan]', `Failed to get ScanResult by promise with options.`); 457 } catch (error) { 458 logger.error('[customscan]', `Failed to detectBarcode. Code: ${error.code}, message: ${error.message}`); 459 } 460 461 // 停止扫描 462 this.stopCustomScan(); 463 return retVal; 464 } 465} 466 467@Observed 468export class ScanResults { 469 public code: number; 470 public data: scanBarcode.ScanResult[]; 471 public size: number; 472 public uri: string; 473 474 constructor() { 475 this.code = 0; 476 this.data = []; 477 this.size = 0; 478 this.uri = ''; 479 } 480} 481 482 483