• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}