• 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, display, LengthMetrics, mediaquery } from '@kit.ArkUI';
17import { image } from '@kit.ImageKit';
18import { BusinessError, commonEventManager } from '@kit.BasicServicesKit';
19import { hilog } from '@kit.PerformanceAnalysisKit';
20import { systemParameterEnhance } from '@kit.BasicServicesKit';
21import { Animator as animator, AnimatorResult, componentUtils } from '@kit.ArkUI';
22import { AnimatorOptions } from '@ohos.animator';
23
24const LOG_TAG: string = 'CustomAppBar';
25const VIEW_WIDTH: number = 80;
26const VIEW_HEIGHT: number = 36;
27const BUTTON_SIZE: number = 40;
28const IMAGE_SIZE: string = '20vp';
29const MENU_RADIUS: string = '20vp';
30const DIVIDER_HEIGHT: string = '16vp';
31const BORDER_WIDTH: string = '1px';
32const VIEW_MARGIN_RIGHT: number = 8;
33const ICON_SIZE: number = 27;
34const ICON_FILL_COLOR_DEFAULT: string = '#182431';
35const BORDER_COLOR_DEFAULT: string = '#33000000';
36const MENU_BACK_COLOR: string = '#99FFFFFF';
37const MENU_BACK_BLUR: number = 5;
38const MENU_MARGIN_TOP: number = 10;
39const SM_MENU_MARGIN_END: number = 16;
40const MD_MENU_MARGIN_END: number = 24;
41const LG_MENU_MARGIN_END: number = 32;
42// 半屏参数
43const BUTTON_IMAGE_SIZE: number = 18;
44const HALF_CONTAINER_BORDER_SIZE: number = 32;
45const HALF_BUTTON_BACK_COLOR: string = '#0D000000';
46const HALF_BUTTON_IMAGE_COLOR: string = '#0C000000';
47const HALF_MENU_MARGIN: number = 16;
48const EYELASH_HEIGHT: number = 36;
49const CHEVRON_HEIGHT: number = 20;
50const CHEVRON_WIDTH: number = 10;
51const CHEVRON_MARGIN: number = 4;
52const TITLE_FONT_SIZE: number = 14;
53const TITLE_LINE_HEIGHT: number = 16;
54const TITLE_MARGIN_RIGHT: number = 12;
55const TITLE_MARGIN_TOP: number = 8;
56const TITLE_LABEL_MARGIN: number = 8.5;
57const TITLE_CONSTRAINT_SIZE: string = 'calc(100% - 73.5vp)';
58const MD_WIDTH: number = 480;
59const LG_WIDTH_LIMIT: number = 0.6;
60const LG_WIDTH_HEIGHT_RATIO: number = 1.95;
61const PRIVACY_MARGIN: number = 12;
62const PRIVACY_FONT_SIZE: string = '12vp';
63const PRIVACY_TEXT_MARGIN_START: number = 4;
64const PRIVACY_TEXT_MARGIN_END: number = 8;
65const PRIVACY_CONSTRAINT_SIZE: string = 'calc(100% - 136vp)';
66const ARKUI_APP_BAR_COLOR_CONFIGURATION: string = 'arkui_app_bar_color_configuration';
67const ARKUI_APP_BAR_MENU_SAFE_AREA: string = 'arkui_app_bar_menu_safe_area';
68const ARKUI_APP_BAR_CONTENT_SAFE_AREA: string = 'arkui_app_bar_content_safe_area';
69
70const ARKUI_APP_BAR_BAR_INFO: string = 'arkui_app_bar_info';
71const ARKUI_APP_BAR_SCREEN: string = 'arkui_app_bar_screen';
72const ARKUI_APP_BG_COLOR: string = 'arkui_app_bg_color';
73const ARKUI_APP_BAR_SERVICE_PANEL: string = 'arkui_app_bar_service_panel';
74const ARKUI_APP_BAR_CLOSE: string = 'arkui_app_bar_close';
75const ARKUI_APP_BAR_PROVIDE_SERVICE: string = 'arkui_app_bar_provide_service';
76const EVENT_NAME_CUSTOM_APP_BAR_MENU_CLICK = 'arkui_custom_app_bar_menu_click';
77const EVENT_NAME_CUSTOM_APP_BAR_CLOSE_CLICK = 'arkui_custom_app_bar_close_click';
78const EVENT_NAME_CUSTOM_APP_BAR_DID_BUILD = 'arkui_custom_app_bar_did_build';
79const EVENT_NAME_CUSTOM_APP_BAR_CREATE_SERVICE_PANEL = 'arkui_custom_app_bar_create_service_panel';
80const ARKUI_APP_BAR_MAXIMIZE: string = 'arkui_app_bar_maximize';
81const ARKUI_APP_BAR_PRIVACY_AUTHORIZE: string = 'arkui_app_bar_privacy_authorize';
82
83/**
84 * 适配不同颜色模式集合
85 */
86class ColorGroup {
87  public light: string = '#000000';
88  public dark: string = '#FFFFFF';
89
90  constructor(light: string, dark: string) {
91    this.light = light;
92    this.dark = dark;
93  }
94}
95
96enum BreakPointsType {
97  NONE = 'NONE',
98  SM = 'SM',
99  MD = 'MD',
100  LG = 'LG'
101}
102
103const menuMarginEndMap: Map<BreakPointsType, number> = new Map<BreakPointsType, number>([
104  [BreakPointsType.NONE, SM_MENU_MARGIN_END],
105  [BreakPointsType.SM, SM_MENU_MARGIN_END],
106  [BreakPointsType.MD, MD_MENU_MARGIN_END],
107  [BreakPointsType.LG, LG_MENU_MARGIN_END]
108]);
109
110const colorMap: Map<string, ColorGroup> = new Map<string, ColorGroup>([
111  [ICON_FILL_COLOR_DEFAULT, new ColorGroup('#182431', '#e5ffffff')],
112  [BORDER_COLOR_DEFAULT, new ColorGroup('#33182431', '#4Dffffff')],
113  [MENU_BACK_COLOR, new ColorGroup('#99FFFFFF', '#33000000')],
114  [HALF_BUTTON_BACK_COLOR, new ColorGroup('#0D000000', '#19FFFFFF')],
115  [HALF_BUTTON_IMAGE_COLOR, new ColorGroup('#000000', '#FFFFFF')]
116]);
117
118@Entry
119@Component
120export struct CustomAppBar {
121  @State menuResource: Resource = {
122    bundleName: '',
123    moduleName: '',
124    params: [],
125    id: 125830217,
126    type: 20000
127  };
128  @State closeResource: Resource = {
129    bundleName: '',
130    moduleName: '',
131    params: [],
132    id: 125831084,
133    type: 20000
134  };
135  @State privacyResource: Resource = {
136    bundleName: '',
137    moduleName: '',
138    params: [],
139    id: 125835516,
140    type: 20000
141  };
142  @State menuFillColor: string = this.getResourceColor(ICON_FILL_COLOR_DEFAULT);
143  @State closeFillColor: string = this.getResourceColor(ICON_FILL_COLOR_DEFAULT);
144  @State menubarBorderColor: string = this.getResourceColor(BORDER_COLOR_DEFAULT);
145  @State menubarBackColor: string = this.getResourceColor(MENU_BACK_COLOR);
146  @State dividerBackgroundColor: string = this.getResourceColor(BORDER_COLOR_DEFAULT);
147  @State halfButtonBackColor: string = this.getResourceColor(HALF_BUTTON_BACK_COLOR);
148  @State halfButtonImageColor: string = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR);
149  @State privacyImageColor: string = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR);
150
151  @State contentMarginTop: number = 0;
152  @State contentMarginLeft: number = 0;
153  @State contentMarginRight: number = 0;
154  @State contentMarginBottom: number = 0;
155  @State menuMarginEnd: number = SM_MENU_MARGIN_END;
156  // 半屏参数
157  @State isHalfScreen: boolean = true;
158  @State containerHeight: string | number = '0%';
159  @State containerWidth: string | number = '100%';
160  @State stackHeight: string = '100%';
161  @State titleOpacity: number = 0;
162  @State buttonOpacity: number = 1;
163  @State titleHeight: number = 0;
164  @State titleOffset: number = 0;
165  @State maskOpacity: number = 0;
166  @State maskBlurScale: number = 0;
167  @State contentBgColor: ResourceColor = '#FFFFFFFF';
168  @State statusBarHeight: number = 0;
169  @State ratio: number | undefined = undefined;
170  @State @Watch('onBreakPointChange') breakPoint: BreakPointsType = BreakPointsType.NONE;
171  @State serviceMenuRead: string = this.getStringByResourceToken(ARKUI_APP_BAR_SERVICE_PANEL);
172  @State closeRead: string = this.getStringByResourceToken(ARKUI_APP_BAR_CLOSE);
173  @State maximizeRead: string = this.getStringByResourceToken(ARKUI_APP_BAR_MAXIMIZE);
174  @State provideService: string = '';
175  @State privacyWidth: string = '0';
176  @State privacySymbolOpacity: number = 0;
177  @State angle: string = '-90deg';
178  @State buttonSize: number = BUTTON_SIZE;
179  @State privacyTextOpacity: number = 0;
180  @State dividerOpacity: number = 0;
181  @State isShowPrivacyAnimation: boolean = false;
182  @State privacyAuthText: string = '';
183  private isHalfToFullScreen: boolean = false;
184  private isDark: boolean = true;
185  private bundleName: string = '';
186  @State labelName: string = '';
187  private isHalfScreenCompFirstLaunch: boolean = true;
188  private icon: Resource | string | PixelMap = $r('sys.media.ohos_app_icon');
189  private fullContentMarginTop: number = 0;
190  private deviceBorderRadius: string = '0';
191  private privacyAnimator: AnimatorResult | undefined = undefined;
192  private smListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(0vp<width) and (width<600vp)');
193  private mdListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(600vp<=width) and (width<840vp)');
194  private lgListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(840vp<=width)');
195  // 订阅者信息, 用于保存创建成功的订阅者对象,后续使用其完成订阅及退订的动作
196  private subscriber: commonEventManager.CommonEventSubscriber | null = null;
197  // 事件列表
198  private subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
199    events: ['usual.event.PRIVACY_STATE_CHANGED']
200  };
201
202  aboutToDisappear(): void {
203    this.smListener.off('change');
204    this.mdListener.off('change');
205    this.lgListener.off('change');
206    if (this.subscriber !== null) {
207      commonEventManager.unsubscribe(this.subscriber, (err) => {
208        if (err) {
209          hilog.error(0x3900, LOG_TAG, `unsubscribe err callback, message is ${err.message}`);
210        } else {
211          this.subscriber = null;
212        }
213      });
214    }
215  }
216
217  /**
218   * 注册监听隐私协议状态
219   */
220  private subscribePrivacyState(): void {
221    try {
222      // 创建订阅者
223      commonEventManager.createSubscriber(this.subscribeInfo).then((commonEventSubscriber) => {
224        this.subscriber = commonEventSubscriber;
225        // 订阅公共事件
226        try {
227          commonEventManager.subscribe(this.subscriber, (err, data) => {
228            if (err) {
229              hilog.error(0x3900, LOG_TAG, `subscribe failed, code is ${err?.code}, message is ${err?.message}`);
230              return;
231            }
232            let result = JSON.parse(data?.data ?? '{}')?.resultType as number;
233            // privacyMgmtType:1 隐私同意完整模式
234            if (result === 1) {
235              if (this.isHalfScreen) {
236                return;
237              }
238              this.isShowPrivacyAnimation = true;
239              this.startPrivacyAnimation();
240            }
241          });
242        } catch (error) {
243          hilog.error(0x3900, LOG_TAG, `init Subscriber failed, code is ${error?.code}, message is ${error?.message}`);
244        }
245      }).catch((error: BusinessError) => {
246        hilog.error(0x3900, LOG_TAG, `createSubscriber failed, code is ${error?.code}, message is ${error?.message}`);
247      });
248    } catch (error) {
249      hilog.error(0x3900, LOG_TAG,
250        `subscribePrivacyState failed, code is ${error?.code}, message is ${error?.message}`);
251    }
252  }
253
254  getDeviceRadiusConfig(): void {
255    try {
256      this.deviceBorderRadius = systemParameterEnhance.getSync('const.product.device_radius');
257      hilog.info(0x3900, LOG_TAG, `read device_radius success, device_radius: ${this.deviceBorderRadius}`);
258    } catch (error) {
259      hilog.error(0x3900, LOG_TAG, `read device_radius failed`);
260    }
261  }
262
263  initBreakPointListener(): void {
264    this.smListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
265      if (mediaQueryResult.matches) {
266        this.breakPoint = BreakPointsType.SM;
267      }
268    })
269    this.mdListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
270      if (mediaQueryResult.matches) {
271        this.breakPoint = BreakPointsType.MD;
272      }
273    })
274    this.lgListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
275      if (mediaQueryResult.matches) {
276        this.breakPoint = BreakPointsType.LG;
277      }
278    })
279  }
280
281  /**
282   * 半屏嵌入式定制使用,当半屏嵌入式组件首次被拉起或者屏幕宽度断点发生变化时被调用
283   * 被调用时更新半屏嵌入式组件的宽高比例
284   */
285  updateRatio(): void {
286    // 屏幕断点为LG或MD时设置成直板机的宽高尺寸比,断点为SM时设置成undefined从而使控制尺寸比的字段失效
287    const isRatioBeUndefined = this.breakPoint === BreakPointsType.LG || this.breakPoint === BreakPointsType.MD;
288    this.ratio = isRatioBeUndefined ? 1 / LG_WIDTH_HEIGHT_RATIO : undefined;
289  }
290
291  onBreakPointChange(): void {
292    if (menuMarginEndMap.has(this.breakPoint)) {
293      this.menuMarginEnd = menuMarginEndMap.get(this.breakPoint) as number;
294    }
295    if (this.isHalfScreen) {
296      if (this.breakPoint === BreakPointsType.SM) {
297        this.containerWidth = '100%';
298      } else if (this.breakPoint === BreakPointsType.MD) {
299        this.containerWidth = MD_WIDTH;
300      } else if (this.breakPoint === BreakPointsType.LG) {
301        try {
302          let displayData = display.getDefaultDisplaySync();
303          let windowWidth = px2vp(displayData.width);
304          let windowHeight = px2vp(displayData.height);
305          this.containerWidth = windowWidth > windowHeight ? windowHeight * LG_WIDTH_LIMIT : windowWidth * LG_WIDTH_LIMIT;
306        } catch (error) {
307          hilog.error(0x3900, LOG_TAG, `getDefaultDisplaySync failed, code is ${error?.code}, message is ${error?.message}`);
308        }
309      }
310    }
311    if (!this.isHalfScreenCompFirstLaunch) {
312      this.updateRatio();
313    }
314  }
315
316  parseBoolean(value: string): boolean {
317    if (value === 'true') {
318      return true;
319    }
320    return false;
321  }
322
323  getResourceColor(defaultColor: string): string {
324    if (colorMap.has(defaultColor)) {
325      const colorGroup = colorMap.get(defaultColor);
326      if (colorGroup) {
327        return this.isDark ? colorGroup.dark : colorGroup.light;
328      }
329    }
330    return defaultColor;
331  }
332
333  getStringByResourceToken(resName: string, value?: string): string {
334    try {
335      if (value) {
336        return getContext(this).resourceManager.getStringByNameSync(resName, value);
337      }
338      return getContext(this).resourceManager.getStringByNameSync(resName);
339    } catch (err) {
340      hilog.error(0x3900, LOG_TAG, `getAccessibilityDescription, error: ${err.toString()}`);
341    }
342    return '';
343  }
344
345  updateStringByResource(): void {
346    if (this.isHalfScreen) {
347      this.provideService = this.getStringByResourceToken(ARKUI_APP_BAR_PROVIDE_SERVICE, this.labelName);
348      this.maximizeRead = this.getStringByResourceToken(ARKUI_APP_BAR_MAXIMIZE);
349    }
350    this.closeRead = this.getStringByResourceToken(ARKUI_APP_BAR_CLOSE);
351    this.serviceMenuRead = this.getStringByResourceToken(ARKUI_APP_BAR_SERVICE_PANEL);
352    this.privacyAuthText = this.getStringByResourceToken(ARKUI_APP_BAR_PRIVACY_AUTHORIZE);
353  }
354
355  /**
356   * atomicservice侧的事件变化回调
357   * @param eventName 事件名称
358   * @param param 事件参数
359   */
360  setCustomCallback(eventName: string, param: string): void {
361    if (param === null || param === '' || param === undefined) {
362      hilog.error(0x3900, LOG_TAG, 'invalid params');
363      return;
364    }
365    if (eventName === ARKUI_APP_BAR_COLOR_CONFIGURATION) {
366      this.onColorConfigurationUpdate(this.parseBoolean(param));
367    } else if (eventName === ARKUI_APP_BAR_MENU_SAFE_AREA) {
368      if (this.statusBarHeight === px2vp(Number(param))) {
369        return;
370      }
371      this.statusBarHeight = Number(param);
372      this.titleHeight = EYELASH_HEIGHT + 2 * TITLE_MARGIN_TOP + this.statusBarHeight;
373    } else if (eventName === ARKUI_APP_BAR_CONTENT_SAFE_AREA) {
374      //top left right bottom
375      let splitArray: string[] = param.split('|');
376      if (splitArray.length < 4) {
377        return;
378      }
379      this.contentMarginTop = this.isHalfScreen ? 0 : Number(splitArray[0]);
380      this.fullContentMarginTop = Number(splitArray[0]);
381      this.contentMarginLeft = Number(splitArray[1]);
382      this.contentMarginRight = Number(splitArray[2]);
383      this.contentMarginBottom = Number(splitArray[3]);
384    } else if (eventName === ARKUI_APP_BAR_BAR_INFO) {
385      let splitArray: string[] = param.split('|');
386      if (splitArray.length < 2) {
387        return;
388      }
389      this.bundleName = splitArray[0];
390      this.labelName = splitArray[1];
391      this.updateStringByResource();
392    } else if (eventName === ARKUI_APP_BAR_SCREEN) {
393      this.isHalfScreen = this.parseBoolean(param);
394      this.initBreakPointListener();
395    } else if (eventName === ARKUI_APP_BG_COLOR) {
396      if (this.isHalfScreen) {
397        this.contentBgColor = Color.Transparent;
398      } else {
399        this.contentBgColor = param;
400      }
401    }
402  }
403
404  /**
405   * 颜色变化设置
406   * @param isDark 是否是深色模式
407   */
408  onColorConfigurationUpdate(isDark: boolean): void {
409    this.isDark = isDark;
410    this.menuFillColor = this.getResourceColor(ICON_FILL_COLOR_DEFAULT);
411    this.closeFillColor = this.getResourceColor(ICON_FILL_COLOR_DEFAULT);
412    this.menubarBorderColor = this.getResourceColor(BORDER_COLOR_DEFAULT);
413    this.dividerBackgroundColor = this.getResourceColor(BORDER_COLOR_DEFAULT);
414    this.menubarBackColor = this.getResourceColor(MENU_BACK_COLOR);
415    this.halfButtonBackColor = this.getResourceColor(HALF_BUTTON_BACK_COLOR);
416    this.halfButtonImageColor = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR);
417    this.privacyImageColor = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR);
418  }
419
420  /**
421   * 标题栏图标回调
422   * @param pixelMap
423   */
424  setAppIcon(pixelMap: image.PixelMap): void {
425    this.icon = pixelMap;
426  }
427
428  /**
429   * 服务面板按钮点击回调
430   */
431  onMenuButtonClick(): void {
432  }
433
434  /**
435   * 关闭按钮点击回调
436   */
437  onCloseButtonClick(): void {
438  }
439
440  /**
441   * 点击title栏
442   */
443  onEyelashTitleClick(): void {
444  }
445
446  /**
447   * 触发构建回调
448   */
449  onDidBuild(): void {
450  }
451
452  /**
453   * 半屏拉起动效
454   */
455  halfScreenShowAnimation(): void {
456    animateTo({
457      duration: 250,
458      curve: Curve.Sharp,
459    }, () => {
460      this.maskOpacity = 0.3;
461      this.maskBlurScale = 1;
462    });
463    animateTo({
464      duration: 250,
465      curve: curves.interpolatingSpring(0, 1, 328, 36),
466    }, () => {
467      this.containerHeight = '100%';
468      this.updateRatio();
469    });
470    // 标题栏渐显
471    animateTo({
472      duration: 100,
473      curve: curves.cubicBezierCurve(0.2, 0, 0.2, 1),
474    }, () => {
475      this.titleOpacity = 1;
476    });
477    this.isHalfScreenCompFirstLaunch = false;
478  }
479
480  /**
481   * 半屏放大至全屏动效
482   */
483  expendContainerAnimation(): void {
484    this.isHalfToFullScreen = true;
485    animateTo({
486      duration: 150,
487      curve: curves.interpolatingSpring(0, 1, 328, 36),
488      onFinish: () => {
489        this.contentBgColor = '#FFFFFF';
490      }
491    }, () => {
492      this.containerWidth = '100%';
493      this.contentMarginTop = this.fullContentMarginTop;
494      this.titleOffset = -this.titleHeight;
495      this.isHalfScreen = false;
496    });
497    // 标题栏渐隐
498    animateTo({
499      duration: 100,
500      curve: curves.cubicBezierCurve(0.2, 0, 0.2, 1),
501    }, () => {
502      this.titleOpacity = 0;
503    });
504  }
505
506  /**
507   * 嵌入式关闭动效
508   */
509  closeContainerAnimation(): void {
510    if (this.isHalfScreen) {
511      this.closeHalfContainerAnimation();
512      return;
513    }
514    if (this.isHalfToFullScreen) {
515      // 关闭弹框
516      animateTo({
517        duration: 250,
518        curve: curves.interpolatingSpring(0, 1, 328, 36),
519        onFinish: () => {
520          this.onCloseButtonClick();
521        }
522      }, () => {
523        this.stackHeight = '0%';
524      });
525    } else {
526      this.onCloseButtonClick();
527    }
528    this.isHalfScreenCompFirstLaunch = true;
529  }
530
531  closeHalfContainerAnimation() {
532    // 关闭弹框
533    animateTo({
534      duration: 250,
535      curve: curves.interpolatingSpring(0, 1, 328, 36),
536      onFinish: () => {
537        this.onCloseButtonClick();
538      }
539    }, () => {
540      this.containerHeight = '0%';
541      this.ratio = undefined;
542    });
543    // 蒙层渐隐
544    animateTo({
545      duration: 250,
546      curve: Curve.Sharp,
547    }, () => {
548      this.maskOpacity = 0;
549      this.maskBlurScale = 0;
550    });
551    // 标题栏渐隐
552    animateTo({
553      duration: 100,
554      curve: curves.cubicBezierCurve(0.2, 0, 0.2, 1),
555    }, () => {
556      this.titleOpacity = 0;
557    });
558  }
559
560  /**
561   * 开始隐私标识动效
562   */
563  private startPrivacyAnimation(): void {
564    animateTo({
565      curve: curves.interpolatingSpring(2, 1, 328, 26),
566    }, () => {
567      this.privacyWidth = '';
568    });
569    animateTo({
570      duration: 250,
571      curve: Curve.Sharp,
572      delay: 100,
573    }, () => {
574      this.privacyTextOpacity = 1;
575    });
576    animateTo({
577      delay: 200,
578      curve: curves.interpolatingSpring(2, 1, 500, 26),
579    }, () => {
580      this.angle = '0';
581    });
582    animateTo({
583      duration: 100,
584      delay: 200,
585      curve: Curve.Sharp,
586    }, () => {
587      this.privacySymbolOpacity = 1;
588      this.dividerOpacity = 1;
589    });
590    // 延迟5s后开始退出动画
591    setTimeout(() => {
592      this.initPrivacyAnimator();
593      animateTo({
594        duration: 50,
595        curve: Curve.Sharp,
596      }, () => {
597        this.dividerOpacity = 0;
598      });
599      animateTo({
600        duration: 100,
601        curve: Curve.Sharp,
602      }, () => {
603        this.privacyTextOpacity = 0;
604      });
605      animateTo({
606        duration: 150,
607        curve: Curve.Sharp,
608      }, () => {
609        this.privacySymbolOpacity = 0;
610      });
611      this.privacyAnimator?.play();
612    }, 5000);
613  }
614
615  /**
616   * 隐私标识动效退出,menubar长度缩小帧动画初始化
617   */
618  private initPrivacyAnimator(): void {
619    let privacyTextLength = px2vp(componentUtils.getRectangleById('AtomicServiceMenuPrivacyId').size.width);
620    let options: AnimatorOptions = {
621      duration: 500,
622      easing: 'interpolating-spring(1, 1, 328, 26)',
623      delay: 0,
624      fill: 'forwards',
625      direction: 'normal',
626      iterations: 1,
627      begin: privacyTextLength + PRIVACY_MARGIN + PRIVACY_TEXT_MARGIN_START + PRIVACY_TEXT_MARGIN_END,
628      end: 0
629    };
630    this.privacyAnimator = animator.create(options);
631    this.privacyAnimator.onFrame = (value: number) => {
632      // 当动画帧值小于0.01时,不做动画。
633      if (value <= 0 && Math.abs(value) < 0.01) {
634        this.buttonSize = BUTTON_SIZE;
635        this.privacyWidth = '0';
636        return;
637      }
638      // 当动画帧值小于0时,对menu按钮做动画;大于0时,对隐私动效宽度做动画。
639      if (value < 0) {
640        this.buttonSize = BUTTON_SIZE + value;
641      } else {
642        this.privacyWidth = JSON.stringify(value);
643      }
644    }
645    this.privacyAnimator.onFinish = () => {
646      this.isShowPrivacyAnimation = false;
647    };
648  }
649
650  @Builder
651  privacySecurityLabel() {
652    Row() {
653      SymbolGlyph(this.privacyResource)
654        .fontSize(IMAGE_SIZE)
655        .fontColor([this.privacyImageColor, Color.Blue])
656        .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
657        .margin({ start: LengthMetrics.vp(PRIVACY_MARGIN) })
658        .opacity(this.privacySymbolOpacity)
659        .rotate({
660          x: 0,
661          y: 1,
662          z: 0,
663          centerX: '50%',
664          centerY: '50%',
665          angle: this.angle
666        })
667      Text(this.privacyAuthText)
668        .fontSize(PRIVACY_FONT_SIZE)
669        .fontWeight(FontWeight.Regular)
670        .textAlign(TextAlign.Center)
671        .padding({
672          start: LengthMetrics.vp(PRIVACY_TEXT_MARGIN_START),
673          end: LengthMetrics.vp(PRIVACY_TEXT_MARGIN_END)
674        })
675        .textAlign(TextAlign.Start)
676        .textOverflow({ overflow: TextOverflow.Ellipsis })
677        .wordBreak(WordBreak.BREAK_WORD)
678        .opacity(this.privacyTextOpacity)
679        .ellipsisMode(EllipsisMode.END)
680        .constraintSize({ maxWidth: PRIVACY_CONSTRAINT_SIZE })
681        .fontColor($r('sys.color.ohos_id_color_text_primary'))
682        .renderFit(RenderFit.RESIZE_FILL)
683        .maxLines(1)
684        .id('AtomicServiceMenuPrivacyId')
685    }
686    .width(this.privacyWidth)
687
688    Divider()
689      .id('AtomicServiceDividerId')
690      .vertical(true)
691      .color(this.dividerBackgroundColor)
692      .lineCap(LineCapStyle.Round)
693      .strokeWidth(BORDER_WIDTH)
694      .height(DIVIDER_HEIGHT)
695      .opacity(this.dividerOpacity)
696  }
697
698  @Builder
699  fullScreenMenubar() {
700    Row() {
701      Row() {
702        if (this.isShowPrivacyAnimation) {
703          this.privacySecurityLabel()
704        }
705
706        Button() {
707          Image(this.menuResource)
708            .id('AtomicServiceMenuIconId')
709            .width(IMAGE_SIZE)
710            .height(IMAGE_SIZE)
711            .fillColor(this.menuFillColor)
712            .draggable(false)
713            .interpolation(ImageInterpolation.High)
714        }
715        .id('AtomicServiceMenuId')
716        .type(ButtonType.Normal)
717        .borderRadius({ topLeft: MENU_RADIUS, bottomLeft: MENU_RADIUS })
718        .backgroundColor(Color.Transparent)
719        .width(this.buttonSize)
720        .height(VIEW_HEIGHT)
721        .accessibilityText(this.serviceMenuRead)
722        .gesture(TapGesture().onAction(() => {
723          this.onMenuButtonClick();
724        }))
725
726        Divider()
727          .id('AtomicServiceDividerId')
728          .vertical(true)
729          .color(this.dividerBackgroundColor)
730          .lineCap(LineCapStyle.Round)
731          .strokeWidth(BORDER_WIDTH)
732          .height(DIVIDER_HEIGHT)
733
734        Button() {
735          Image(this.closeResource)
736            .id('AtomicServiceCloseIconId')
737            .width(IMAGE_SIZE)
738            .height(IMAGE_SIZE)
739            .fillColor(this.closeFillColor)
740            .draggable(false)
741            .interpolation(ImageInterpolation.High)
742        }
743        .id('AtomicServiceCloseId')
744        .type(ButtonType.Normal)
745        .backgroundColor(Color.Transparent)
746        .borderRadius({ topRight: MENU_RADIUS, bottomRight: MENU_RADIUS })
747        .width(BUTTON_SIZE)
748        .height(VIEW_HEIGHT)
749        .accessibilityText(this.closeRead)
750        .gesture(TapGesture().onAction(() => {
751          this.closeContainerAnimation();
752        }))
753      }
754      .borderRadius(MENU_RADIUS)
755      .borderWidth(BORDER_WIDTH)
756      .borderColor(this.menubarBorderColor)
757      .backgroundColor(this.menubarBackColor)
758      .backgroundEffect({
759        radius: MENU_BACK_BLUR,
760        color: this.menubarBackColor
761      })
762      .height(VIEW_HEIGHT)
763      .align(Alignment.Top)
764      .draggable(false)
765      .geometryTransition('menubar')
766      .id('AtomicServiceMenubarId')
767    }
768    .id('AtomicServiceMenubarRowId')
769    .margin({
770      top: LengthMetrics.vp(this.statusBarHeight + MENU_MARGIN_TOP),
771      end: LengthMetrics.vp(this.menuMarginEnd)
772    })
773    .justifyContent(FlexAlign.End)
774    .height(VIEW_HEIGHT)
775    .hitTestBehavior(HitTestMode.Transparent)
776    .width('100%')
777  }
778
779  @Builder
780  eyelashTitle() {
781    Column() {
782      Row() {
783        Row() {
784          Image(this.icon).height(ICON_SIZE).width(ICON_SIZE)
785            .margin({
786              start: LengthMetrics.vp(CHEVRON_MARGIN)
787            })
788          Text(this.provideService)
789            .fontSize(TITLE_FONT_SIZE)
790            .lineHeight(TITLE_LINE_HEIGHT)
791            .fontWeight(FontWeight.Medium)
792            .fontColor('#FFFFFF')
793            .margin({ start: LengthMetrics.vp(TITLE_LABEL_MARGIN) })
794            .maxLines(1)
795            .textOverflow({ overflow: TextOverflow.Ellipsis })
796            .ellipsisMode(EllipsisMode.END)
797            .constraintSize({ maxWidth: TITLE_CONSTRAINT_SIZE })
798          SymbolGlyph($r('sys.symbol.chevron_right'))
799            .height(CHEVRON_HEIGHT)
800            .width(CHEVRON_WIDTH)
801            .margin({ start: LengthMetrics.vp(CHEVRON_MARGIN), end: LengthMetrics.vp(CHEVRON_MARGIN) })
802            .fontColor([Color.White])
803        }
804        .height(EYELASH_HEIGHT)
805        .stateStyles({
806          focused: {
807            .backgroundColor('#0D000000')
808          },
809          pressed: {
810            .backgroundColor('#1A000000')
811          },
812          normal: {
813            .backgroundColor(Color.Transparent)
814          }
815        })
816        .borderRadius(EYELASH_HEIGHT / 2)
817        .onClick(() => {
818          this.onEyelashTitleClick();
819        })
820        .margin({ start: LengthMetrics.vp(TITLE_MARGIN_RIGHT) })
821      }
822      .margin({
823        top: LengthMetrics.vp(this.statusBarHeight + TITLE_MARGIN_TOP),
824        bottom: LengthMetrics.vp(TITLE_MARGIN_TOP)
825      })
826      .opacity(this.titleOpacity)
827      .justifyContent(FlexAlign.Start)
828      .width('100%')
829      .hitTestBehavior(HitTestMode.Transparent)
830    }
831    .justifyContent(FlexAlign.Start)
832    .height(this.titleHeight)
833    .offset({ y: this.titleOffset })
834    .hitTestBehavior(HitTestMode.Transparent)
835  }
836
837  @Builder
838  halfScreenMenuBar() {
839    Column() {
840      Row() {
841        Row() {
842          Button({ type: ButtonType.Circle }) {
843            SymbolGlyph($r('sys.symbol.arrow_up_left_and_arrow_down_right'))
844              .fontSize(BUTTON_IMAGE_SIZE)
845              .fontWeight(FontWeight.Medium)
846              .fontColor([this.halfButtonImageColor])
847          }.width(BUTTON_SIZE).height(BUTTON_SIZE).backgroundColor(this.halfButtonBackColor)
848          .onClick(() => {
849            this.expendContainerAnimation();
850          })
851          .accessibilityText(this.maximizeRead)
852          Button({ type: ButtonType.Circle }) {
853            SymbolGlyph($r('sys.symbol.xmark'))
854              .fontSize(BUTTON_IMAGE_SIZE)
855              .fontWeight(FontWeight.Medium)
856              .fontColor([this.halfButtonImageColor])
857          }
858          .width(BUTTON_SIZE)
859          .height(BUTTON_SIZE)
860          .margin({
861            start: LengthMetrics.vp(VIEW_MARGIN_RIGHT),
862          })
863          .backgroundColor(this.halfButtonBackColor)
864          .onClick(() => {
865            this.closeContainerAnimation();
866          })
867          .accessibilityText(this.closeRead)
868        }
869        .geometryTransition('menubar')
870        .justifyContent(FlexAlign.End)
871        .transition(TransitionEffect.OPACITY)
872        .borderRadius(MENU_RADIUS)
873        .height(BUTTON_SIZE)
874        .margin({
875          top: LengthMetrics.vp(this.titleHeight + HALF_MENU_MARGIN),
876          end: LengthMetrics.vp(HALF_MENU_MARGIN)
877        })
878      }
879      .width(this.containerWidth)
880      .height(this.containerHeight)
881      .aspectRatio(this.ratio)
882      .alignItems(VerticalAlign.Top)
883      .justifyContent(FlexAlign.End)
884      .opacity(this.buttonOpacity)
885    }.height('100%')
886    .width('100%')
887    .justifyContent(FlexAlign.End)
888    .hitTestBehavior(HitTestMode.Transparent)
889  }
890
891  build() {
892    Column() {
893      Stack({ alignContent: Alignment.TopEnd }) {
894        if (this.isHalfScreen) {
895          // 透明模糊背板
896          Column()
897            .width('100%')
898            .height('100%')
899            .backgroundColor('#262626')
900            .opacity(this.maskOpacity)
901            .foregroundBlurStyle(BlurStyle.BACKGROUND_REGULAR, { colorMode: ThemeColorMode.LIGHT, scale: this.maskBlurScale })
902            .onClick(() => {
903              this.closeContainerAnimation();
904            })
905        }
906        Column() {
907          Column() {
908            if (this.isHalfScreen) {
909              this.eyelashTitle()
910            }
911            Row() {
912            }
913            .padding({
914              top: this.contentMarginTop,
915              left: this.contentMarginLeft,
916              right: this.contentMarginRight,
917              bottom: this.contentMarginBottom
918            })
919            .layoutWeight(1)
920            .backgroundColor(Color.Transparent)
921            .backgroundBlurStyle(this.isHalfScreen ? BlurStyle.COMPONENT_ULTRA_THICK : undefined)
922            .borderRadius({
923              topLeft: this.isHalfScreen ? HALF_CONTAINER_BORDER_SIZE : this.deviceBorderRadius,
924              topRight: this.isHalfScreen ? HALF_CONTAINER_BORDER_SIZE : this.deviceBorderRadius,
925            })
926            .clip(true)
927            .alignItems(VerticalAlign.Bottom)
928            .width('100%')
929            .onClick(() => {
930              // 拦截到背板的点击事件
931            })
932            .id('AtomicServiceStageId')
933          }
934          .height(this.containerHeight)
935          .width(this.containerWidth)
936          .aspectRatio(this.ratio)
937          .justifyContent(FlexAlign.End)
938          .hitTestBehavior(HitTestMode.Transparent)
939          .id('AtomicServiceStageColumnId')
940        }.height('100%')
941        .width('100%')
942        .justifyContent(FlexAlign.End)
943        .hitTestBehavior(HitTestMode.Transparent)
944        .id('AtomicServiceStageLayoutId')
945
946        if (this.isHalfScreen) {
947          this.halfScreenMenuBar()
948        } else {
949          this.fullScreenMenubar()
950        }
951      }
952      .height(this.stackHeight)
953      .width('100%')
954      .backgroundColor(this.contentBgColor)
955      .hitTestBehavior(HitTestMode.Transparent)
956      .id('AtomicServiceContainerId')
957      .onAppear(() => {
958        if (this.isHalfScreen) {
959          this.contentBgColor = Color.Transparent;
960          this.titleHeight = EYELASH_HEIGHT + 2 * TITLE_MARGIN_TOP + this.statusBarHeight;
961          this.halfScreenShowAnimation();
962        } else {
963          this.containerHeight = '100%';
964          this.containerWidth = '100%';
965        }
966        this.updateStringByResource();
967        this.getDeviceRadiusConfig();
968        this.subscribePrivacyState();
969      })
970    }
971    .height('100%')
972    .width('100%')
973    .justifyContent(FlexAlign.End)
974    .backgroundColor(Color.Transparent)
975    .hitTestBehavior(HitTestMode.Transparent)
976  }
977}