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 { curves, promptAction} from '@kit.ArkUI'; 17import { BusinessError } from '@kit.BasicServicesKit'; 18import { vibrator } from '@kit.SensorServiceKit'; 19import { GlobalContext } from '../common/util/GlobalUtil'; 20import { XComponentSize } from '../model/ScanSize' 21import { ScanTitle } from './ScanTitle'; 22import { scanBarcode } from '@kit.ScanKit'; 23import { logger } from '../common/util/Logger'; 24import CommonConstants from '../common/constants/CommonConstants'; 25import CustomScanViewModel, { ScanResults } from '../viewmodel/CustomScanViewModel'; 26import { PromptTone } from '../model/PromptTone'; 27import { funcDelayer } from '../common/util/FunctionUtil'; 28 29/** 30 * 二维码位置的样式 31 */ 32@Extend(Image) 33function selected(scanState: boolean, x: number, y: number) { 34 .width(40) 35 .height(40) 36 .position({ x: x, y: y }) 37 .markAnchor({ x: 20, y: 20 }) 38 .visibility(scanState ? Visibility.Visible : Visibility.Hidden) 39 .draggable(false) 40} 41 42/** 43 * 二维码位置组件 44 */ 45@Component 46export struct CodeLayout { 47 @Consume('subPageStack') subPageStack: NavPathStack; 48 @ObjectLink xComponentSize: XComponentSize; 49 @Prop navHeight: number; 50 @State scanResults: ScanResults = { 51 code: 0, 52 data: [], 53 size: 0, 54 uri: '' 55 } 56 @Prop foldStatus: number = -1; // 折叠状态 57 @State multiCodeScanLocation: number[][] = []; 58 @State multiCodeScanResult: scanBarcode.ScanResult[] = [];// 多个二维码结果集 59 @State isMultiSelected: boolean = false;// 多二维码下是否已选择 60 @State multiSelectedIndex: number = 0;// 多二维码下是否选择的index 61 @State singleCodeX: number = 0;// 单个结果的位置 62 @State singleCodeY: number = 0; 63 @State multiCodeScale: number = 0.3; // 多二维码图案比例参数 64 @State multiCodeOpacity: number = 0; // 透明度设置 65 @State singleCodeScale: number = 0.3; // 单二维码图案比例参数 66 @State singleCodeOpacity: number = 0; // 透明度设置 67 @State fadeOutScale: number = 1; 68 @State fadeOutOpacity: number = 1; 69 @State isPickerDialogShow: boolean = false; 70 @State isShowCode: boolean = true; 71 @Consume('customScanVM') customScanVM: CustomScanViewModel; 72 @Link avPlayer: PromptTone;// 成功扫描二维码的提示音 73 74 aboutToAppear() { 75 // 触发传感器震动 76 this.vibratorPlay(); 77 // 播放二维码扫描成功提示音 78 this.avplayerPlay(); 79 // 处理扫码结果信息 80 for (let i = 0; i < this.scanResults.size; i++) { 81 let scanResult: scanBarcode.ScanResult = this.scanResults.data[i]; 82 this.multiCodeScanResult.push(scanResult); 83 let scanCodeRect: scanBarcode.ScanCodeRect | undefined = scanResult.scanCodeRect; 84 if (scanCodeRect) { 85 this.multiCodeScanLocation.push( 86 [scanCodeRect.left, 87 scanCodeRect.top, 88 scanCodeRect.right, 89 scanCodeRect.bottom] 90 ); 91 } 92 } 93 94 // 只扫描到一个二维码时,单独处理 95 if (this.scanResults.size === 1) { 96 this.multiSelectedIndex = 0; 97 let location = this.multiCodeScanLocation[0]; 98 this.singleCodeX = this.getOffset('x', location); 99 this.singleCodeY = this.getOffset('y', location); 100 } 101 102 } 103 104 aboutToDisappear() { 105 GlobalContext.getContext().setProperty((CommonConstants.GLOBAL_SCAN_SELECT_A_PICTURE), false); 106 this.isPickerDialogShow = false; 107 } 108 109 /** 110 * 单个二维码的位置图片 111 */ 112 @Builder 113 SingleCodeLayout() { 114 Column() { 115 Image($rawfile('scan_selected.svg')) 116 // TODO: 知识点: 在扫描结果返回的水平坐标和纵坐标位置上展示图片 117 .selected(true, this.singleCodeX, this.singleCodeY) 118 .scale({ x: this.singleCodeScale, y: this.singleCodeScale }) 119 .opacity(this.singleCodeOpacity) 120 .onAppear(() => { 121 this.singleCodeBreathe(); 122 }) 123 } 124 .position({ x: 0, y: 0 }) 125 .width('100%') 126 .height('100%') 127 } 128 129 /** 130 * 多个二维码的位置图片渲染 131 */ 132 @Builder 133 MultiCodeLayout(arr: number[], index: number) { 134 Row() { 135 Image($rawfile('scan_selected2.svg')) 136 .width(40) 137 .height(40) 138 .visibility((this.isMultiSelected && this.multiSelectedIndex !== index) ? Visibility.None : Visibility.Visible) 139 .scale({ x: this.multiCodeScale, y: this.multiCodeScale }) 140 .opacity(this.multiCodeOpacity) 141 .onAppear(() => { 142 // 展示动画,因为共用状态变量,只需要第一次执行 143 if (index === 0) { 144 this.multiAppear(); 145 } 146 }) 147 .onClick(() => { 148 // 点击打开二维码信息弹窗 149 this.openMultiCode(arr, index); 150 }) 151 } 152 // TODO: 知识点: 预览流有固定比例,XComponent只能展示部分,返回的扫码结果和当前展示存在一定偏移量 153 .position({ 154 x: this.getOffset('x', arr), 155 y: this.getOffset('y', arr) 156 }) 157 .width(40) 158 .height(40) 159 .markAnchor({ x: 20, y: 20 }) 160 .scale({ x: this.fadeOutScale, y: this.fadeOutScale }) 161 .opacity(this.fadeOutOpacity) 162 .animation({ 163 duration: 200, 164 curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), 165 delay: 0, 166 iterations: 1, 167 playMode: PlayMode.Alternate, 168 }) 169 } 170 171 build() { 172 Stack() { 173 // 如果只有一个结果,渲染单个位置 174 if (this.scanResults.size === 1 && this.isShowCode) { 175 this.SingleCodeLayout(); 176 } else { 177 // 多结果提示文案 178 ScanTitle({ 179 navHeight: this.navHeight, 180 }).width('100%').height('100%') 181 182 // 渲染多二维码位置结果 183 ForEach(this.multiCodeScanLocation, (item: number[], index: number) => { 184 this.MultiCodeLayout(item, index) 185 }, (item: number) => item.toString()) 186 // 点击某一个二维码后,展示选中图案 187 Image($rawfile('scan_selected.svg')) 188 .selected(true, this.singleCodeX, this.singleCodeY) 189 .scale({ x: this.singleCodeScale, y: this.singleCodeScale }) 190 .opacity(this.singleCodeOpacity) 191 .visibility(this.isMultiSelected ? Visibility.Visible : Visibility.None) 192 } 193 } 194 .width('100%') 195 .height('100%') 196 } 197 198 // 计算水平或者竖直的偏移量 199 getOffset(coordinateAxis: string, location: number[]): number { 200 if (coordinateAxis === 'x') { 201 return this.setOffsetXByOrientation(location); 202 } 203 return this.setOffsetYByOrientation(location); 204 } 205 206 setOffsetXByOrientation(location: number[]): number { 207 let offset: number = (location[0] + location[2]) / 2 + this.xComponentSize.offsetX; 208 return offset; 209 } 210 211 setOffsetYByOrientation(location: number[]): number { 212 let offset: number = (location[3] + location[1]) / 2 + this.xComponentSize.offsetY; 213 return offset; 214 } 215 216 // 传感器震动 217 vibratorPlay() { 218 try { 219 vibrator.startVibration({ 220 type: 'time', 221 duration: 100 222 }, { 223 id: 0, 224 usage: 'alarm' 225 }).then((): void => { 226 }, (error: BusinessError) => { 227 logger.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`); 228 }); 229 } catch (err) { 230 let error: BusinessError = err as BusinessError; 231 logger.error(`Failed to play vibration, An unexpected error occurred. Code: ${error.code}, message: ${error.message}`); 232 } 233 } 234 235 // 播放二维码扫描成功提示音 236 avplayerPlay() { 237 if (this.avPlayer) { 238 this.avPlayer.playDrip(); 239 } 240 } 241 242 /** 243 * 多个二维码,点击查看某个二维码信息 244 * @param arr 245 * @param index 246 */ 247 openMultiCode(arr: number[], index: number): void { 248 this.singleCodeX = this.getOffset('x', arr); 249 this.singleCodeY = this.getOffset('y', arr); 250 this.isMultiSelected = true; 251 this.singleCodeScale = 0.3; 252 this.singleCodeOpacity = 0; 253 this.multiSelectedIndex = index || 0; 254 this.fadeOutScale = 0.3; 255 this.fadeOutOpacity = 0; 256 this.showScanResult(this.scanResults.data[index]) 257 } 258 259 /** 260 * 二维码图标呼吸动效 261 */ 262 singleCodeBreathe(): void { 263 this.singleCodeOpacity = 0.3; 264 this.singleCodeScale = 0.3; 265 animateTo({ 266 duration: 300, 267 curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), 268 delay: 0, 269 iterations: 1, 270 playMode: PlayMode.Alternate, 271 onFinish: () => { 272 this.showScanResult(this.scanResults.data[0]) 273 } 274 }, () => { 275 this.singleCodeOpacity = 1; 276 this.singleCodeScale = 1; 277 }); 278 } 279 280 multiAppear(): void { 281 this.multiCodeScale = 0.3; 282 animateTo({ 283 duration: 350, 284 curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), // Animation curve. 285 delay: 0, 286 iterations: 1, 287 playMode: PlayMode.Alternate, 288 onFinish: () => { 289 this.multiAppearEnd(); 290 } 291 }, () => { 292 this.multiCodeScale = 1.1; 293 this.multiCodeOpacity = 1; 294 }); 295 } 296 297 multiAppearEnd(): void { 298 animateTo({ 299 duration: 250, 300 curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), // Animation curve. 301 delay: 0, 302 iterations: 1, 303 playMode: PlayMode.Alternate, 304 onFinish: () => { 305 funcDelayer(() => { 306 this.multiCodeBreathe(); 307 }, 500); 308 } 309 }, () => { 310 this.multiCodeScale = 1; 311 }); 312 } 313 314 /** 315 * 多二维码结果图案的呼吸动效 316 */ 317 multiCodeBreathe(): void { 318 this.multiCodeScale = 1; 319 animateTo({ 320 duration: 300, 321 curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), // Animation curve. 322 delay: 0, 323 iterations: 4, 324 playMode: PlayMode.Alternate, 325 onFinish: () => { 326 funcDelayer(() => { 327 this.multiCodeBreathe(); 328 }, 400); 329 } 330 }, () => { 331 this.multiCodeScale = 0.8; 332 }); 333 } 334 335 /** 336 * 显示扫码结果 337 * @param {scanBarcode.ScanResult} result 扫码结果数据 338 * @returns {Promise<void>} 339 */ 340 async showScanResult(scanResult: scanBarcode.ScanResult): Promise<void> { 341 // 码源信息 342 const originalValue: string = scanResult.originalValue; 343 // 二维码识别结果展示 344 this.subPageStack.pushPathByName(CommonConstants.SUB_PAGE_DETECT_BARCODE, originalValue, true); 345 } 346}