• 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 { bundleManager, common } from '@kit.AbilityKit';
17import { BusinessError } from '@kit.BasicServicesKit';
18import { hilog } from '@kit.PerformanceAnalysisKit';
19import { LengthMetrics, window } from '@kit.ArkUI';
20
21const BUTTON_WIDTH: number = 28;
22const VIEW_HEIGHT: number = 28;
23const IMAGE_SIZE: number = 16;
24const MENU_RADIUS: number = 15.5;
25const DIVIDER_HEIGHT: number = 16.5;
26const DIVIDER_WIDTH: number = 0.5;
27const MENU_BUTTON_MARGIN: number = 2;
28const VIEW_MARGIN_TOP: number = 14;
29const VIEW_MARGIN_RIGHT: number = 24;
30const MENU_BACK_BLUR: number = 5;
31const MENU_BORDER_WIDTH: string = '0.5px';
32
33const ICON_FILL_COLOR_DEFAULT: string = '#182431';
34const BORDER_COLOR_DEFAULT: string = '#33000000';
35const MENU_BACK_COLOR: string = '#99FFFFFF';
36
37const ARKUI_APP_BAR_COLOR_CONFIGURATION: string = 'arkui_app_bar_color_configuration';
38const ARKUI_APP_BAR_CONTENT_SAFE_AREA: string = 'arkui_app_bar_content_safe_area';
39const ARKUI_APP_BG_COLOR: string = 'arkui_app_bg_color';
40const maximizeButtonResourceId: number = 125829923;
41const recoverButtonResourceId: number = 125829925;
42const EVENT_NAME_CUSTOM_APP_BAR_MENU_CLICK = 'arkui_custom_app_bar_menu_click';
43const EVENT_NAME_CUSTOM_APP_BAR_DID_BUILD = 'arkui_custom_app_bar_did_build';
44const EVENT_NAME_MIN_CLICK: string = 'arkui_custom_min_click';
45const EVENT_NAME_CLOSE_CLICK: string = 'arkui_custom_close_click';
46const EVENT_NAME_CUSTOM_MAX_CLICK: string = 'arkui_custom_max_click';
47const ARKUI_APP_BAR_MENU_SAFE_AREA: string = 'arkui_app_bar_menu_safe_area';
48
49class ColorGroup {
50  public light: string = '#000000';
51  public dark: string = '#FFFFFF';
52
53  constructor(light: string, dark: string) {
54    this.light = light;
55    this.dark = dark;
56  }
57}
58
59const colorMap: Map<string, ColorGroup> = new Map<string, ColorGroup>([
60  [ICON_FILL_COLOR_DEFAULT, new ColorGroup('#182431', '#e5ffffff')],
61  [BORDER_COLOR_DEFAULT, new ColorGroup('#33182431', '#4Dffffff')],
62  [MENU_BACK_COLOR, new ColorGroup('#99FFFFFF', '#33000000')],
63]);
64
65@Component
66export struct CustomAppBarForPC {
67  @State menuResource: Resource = {
68    bundleName: '',
69    moduleName: '',
70    params: [],
71    id: 125830217,
72    type: 20000
73  };
74  @State closeResource: Resource = {
75    bundleName: '',
76    moduleName: '',
77    params: [],
78    id: 125831084,
79    type: 20000
80  };
81  @State menuFillColor: string = this.getResourceColor(ICON_FILL_COLOR_DEFAULT);
82  @State menubarBorderColor: string = this.getResourceColor(BORDER_COLOR_DEFAULT);
83  @State menubarBackColor: string = this.getResourceColor(MENU_BACK_COLOR);
84  @State dividerBackgroundColor: string = this.getResourceColor(BORDER_COLOR_DEFAULT);
85  @State contentBgColor: string = '#FFFFFFFF';
86  @State contentMarginTop: string = '0vp';
87  @State contentMarginLeft: string = '0vp';
88  @State contentMarginRight: string = '0vp';
89  @State contentMarginBottom: string = '0vp';
90  @State isAdaptPC: boolean = false;
91  @State maximizeResource: Resource = this.getIconResource(maximizeButtonResourceId);
92  @State statusBarHeight: number = 0;
93
94  private isDark: boolean = true;
95  private windowClass: window.Window | undefined = undefined;
96
97  async aboutToAppear(): Promise<void> {
98    let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE;
99    try {
100      bundleManager.getBundleInfoForSelf(bundleFlags).then((data) => {
101        hilog.info(0x0000, 'testTag', 'getBundleInfoForSelf successfully. Data: %{public}s',
102          JSON.stringify(data.hapModulesInfo[0].deviceTypes));
103        let devicetype = data.hapModulesInfo[0].deviceTypes;
104        for (let i = 0; i < devicetype.length; i++) {
105          if (devicetype[i] === '2in1') {
106            this.isAdaptPC = true;
107            break;
108          }
109        }
110      }).catch((err: BusinessError) => {
111        hilog.error(0x0000, 'testTag', 'getBundleInfoForSelf failed. Cause: %{public}s', err.message);
112      });
113    } catch (err) {
114      let message = (err as BusinessError).message;
115      hilog.error(0x0000, 'testTag', 'getBundleInfoForSelf failed: %{public}s', message);
116    }
117
118    let context = getContext(this) as common.UIAbilityContext;
119    context?.windowStage?.getMainWindow().then(
120      data => {
121        this.windowClass = data;
122        this.windowClass?.setWindowDecorVisible(false);
123        this.windowClass?.setWindowTitleButtonVisible(false, false, false);
124        this.updateMaximizeResource(this.windowClass?.getWindowStatus());
125        this.windowClass?.on('windowStatusChange', (windowStatusType) => {
126          console.info('windowStatusChange  windowStatusType: ' + JSON.stringify(windowStatusType));
127          this.updateMaximizeResource(windowStatusType);
128        });
129        if (!this.isAdaptPC) {
130          this.windowClass?.setWindowTitleMoveEnabled(false);
131        }
132      }
133    ).catch((err: BusinessError) => {
134      if (err.code) {
135        console.error(`Failed to obtain the main window. Cause code: ${err.code}, message: ${err.message}`);
136      }
137    });
138  }
139
140  updateMaximizeResource(windowStatusType: window.WindowStatusType): void {
141    if (windowStatusType === window.WindowStatusType.FULL_SCREEN ||
142      windowStatusType === window.WindowStatusType.SPLIT_SCREEN ||
143      windowStatusType === window.WindowStatusType.MAXIMIZE) {
144      this.maximizeResource = this.getIconResource(recoverButtonResourceId);
145    } else {
146      this.maximizeResource = this.getIconResource(maximizeButtonResourceId);
147    }
148  }
149
150  aboutToDisappear(): void {
151    this.windowClass?.off('windowStatusChange');
152  }
153
154  parseBoolean(value: string): boolean {
155    if (value === 'true') {
156      return true;
157    }
158    return false;
159  }
160
161  getResourceColor(defaultColor: string): string {
162    if (colorMap.has(defaultColor)) {
163      const colorGroup = colorMap.get(defaultColor);
164      if (colorGroup) {
165        return this.isDark ? colorGroup.dark : colorGroup.light;
166      }
167    }
168    return defaultColor;
169  }
170
171  /**
172   * 监听来自arkui侧的回调
173   * @param eventName 事件名
174   * @param param 参数
175   */
176  setCustomCallback(eventName: string, param: string) {
177    if (eventName === ARKUI_APP_BAR_COLOR_CONFIGURATION) {
178      this.onColorConfigurationUpdate(this.parseBoolean(param));
179    } else if (eventName === ARKUI_APP_BAR_CONTENT_SAFE_AREA) {
180      //top left right bottom
181      let splitArray: string[] = param.split('|');
182      if (splitArray.length < 4) {
183        return;
184      }
185      this.statusBarHeight = Number(splitArray[0]);
186      this.contentMarginTop = splitArray[0];
187      this.contentMarginLeft = splitArray[1];
188      this.contentMarginRight = splitArray[2];
189      this.contentMarginBottom = splitArray[3];
190    } else if (eventName === ARKUI_APP_BG_COLOR) {
191      this.contentBgColor = param;
192    }
193  }
194
195  /**
196   * menu按钮点击
197   */
198  onMenuButtonClick(): void {
199  }
200
201  /**
202   * 点击放大按钮
203   */
204  onMaximizeButtonClick(): void {
205  }
206
207  /**
208   * 点击最小化按钮
209   */
210  onMinimizeButtonClick(): void {
211  }
212
213  /**
214   * 点击关闭按钮
215   */
216  onCloseButtonClick(): void {
217  }
218
219  onDidBuild(): void {
220  }
221
222  @Builder
223  dividerLine() {
224    Divider()
225      .id('AtomicServiceDividerId')
226      .vertical(true)
227      .color(this.dividerBackgroundColor)
228      .lineCap(LineCapStyle.Round)
229      .strokeWidth(DIVIDER_WIDTH)
230      .height(DIVIDER_HEIGHT)
231  }
232
233  build() {
234    Column() {
235      Stack({ alignContent: Alignment.TopEnd }) {
236        Row() {
237        }
238        .padding({
239          top: this.contentMarginTop,
240          left: this.contentMarginLeft,
241          right: this.contentMarginRight,
242          bottom: this.contentMarginBottom
243        })
244        .height('100%')
245        .width('100%')
246        .id('AtomicServiceStageId')
247        .backgroundColor(Color.Blue)
248
249        Row() {
250          Row() {
251            Button() {
252              Image(this.menuResource)
253                .width(IMAGE_SIZE)
254                .height(IMAGE_SIZE)
255                .fillColor(this.menuFillColor)
256                .draggable(false)
257                .interpolation(ImageInterpolation.High)
258                .margin({ start: LengthMetrics.vp(MENU_BUTTON_MARGIN) })
259            }
260            .id('AtomicServiceMenuId')
261            .type(ButtonType.Normal)
262            .borderRadius({ topLeft: MENU_RADIUS, bottomLeft: MENU_RADIUS })
263            .backgroundColor(Color.Transparent)
264            .width(BUTTON_WIDTH + MENU_BUTTON_MARGIN)
265            .height(VIEW_HEIGHT)
266            .gesture(TapGesture().onAction(() => {
267              this.onMenuButtonClick();
268            }))
269
270            this.dividerLine()
271
272            if (this.isAdaptPC) {
273              Button() {
274                Image(this.maximizeResource)
275                  .width(IMAGE_SIZE)
276                  .height(IMAGE_SIZE)
277                  .fillColor(this.menuFillColor)
278                  .draggable(false)
279                  .interpolation(ImageInterpolation.High)
280              }
281              .id('AtomicServiceexpendId')
282              .type(ButtonType.Normal)
283              .backgroundColor(Color.Transparent)
284              .width(BUTTON_WIDTH)
285              .height(VIEW_HEIGHT)
286              .gesture(TapGesture().onAction(() => {
287                this.onMaximizeButtonClick();
288              }))
289
290              this.dividerLine()
291            }
292
293            Button() {
294              SymbolGlyph($r('sys.symbol.minus'))
295                .fontSize(IMAGE_SIZE)
296                .fontColor([this.menuFillColor])
297                .draggable(false)
298            }
299            .id('AtomicServiceMinusId')
300            .type(ButtonType.Normal)
301            .backgroundColor(Color.Transparent)
302            .width(BUTTON_WIDTH)
303            .height(VIEW_HEIGHT)
304            .gesture(TapGesture().onAction(() => {
305              this.onMinimizeButtonClick();
306            }))
307
308            this.dividerLine()
309
310            Button() {
311              Image(this.closeResource)
312                .width(IMAGE_SIZE)
313                .height(IMAGE_SIZE)
314                .fillColor(this.menuFillColor)
315                .margin({ end: LengthMetrics.vp(MENU_BUTTON_MARGIN) })
316                .draggable(false)
317                .interpolation(ImageInterpolation.High)
318            }
319            .id('AtomicServiceCloseId')
320            .type(ButtonType.Normal)
321            .backgroundColor(Color.Transparent)
322            .borderRadius({ topRight: MENU_RADIUS, bottomRight: MENU_RADIUS })
323            .width(BUTTON_WIDTH + MENU_BUTTON_MARGIN)
324            .height(VIEW_HEIGHT)
325            .gesture(TapGesture().onAction(() => {
326              this.onCloseButtonClick();
327            }))
328          }
329          .borderRadius(MENU_RADIUS)
330          .borderColor(this.menubarBorderColor)
331          .backgroundColor(this.menubarBackColor)
332          .backdropBlur(MENU_BACK_BLUR)
333          .borderWidth(MENU_BORDER_WIDTH)
334          .borderColor($r('sys.color.icon_fourth'))
335          .height(VIEW_HEIGHT)
336          .align(Alignment.Top)
337          .draggable(false)
338          .id('AtomicServiceMenubarId')
339        }
340        .id('AtomicServiceMenubarRowId')
341        .justifyContent(FlexAlign.End)
342        .margin({ top: LengthMetrics.vp(this.statusBarHeight + VIEW_MARGIN_TOP), end: LengthMetrics.vp(VIEW_MARGIN_RIGHT) })
343        .height(VIEW_HEIGHT)
344        .hitTestBehavior(HitTestMode.Transparent)
345      }
346      .id('AtomicServiceContainerId')
347      .height('100%')
348      .width('100%')
349      .backgroundColor(Color.Transparent)
350      .hitTestBehavior(HitTestMode.Transparent)
351    }
352    .height('100%')
353    .width('100%')
354    .justifyContent(FlexAlign.End)
355    .backgroundColor(this.contentBgColor)
356    .hitTestBehavior(HitTestMode.Transparent)
357  }
358
359  private onColorConfigurationUpdate(isDark: boolean) {
360    this.isDark = isDark;
361    this.menuFillColor = this.getResourceColor(ICON_FILL_COLOR_DEFAULT);
362    this.menubarBorderColor = this.getResourceColor(BORDER_COLOR_DEFAULT);
363    this.dividerBackgroundColor = this.getResourceColor(BORDER_COLOR_DEFAULT);
364    this.menubarBackColor = this.getResourceColor(MENU_BACK_COLOR);
365  }
366
367  private getIconResource(resourceId: number): Resource {
368    return {
369      bundleName: '',
370      moduleName: '',
371      params: [],
372      id: resourceId,
373      type: 20000
374    };
375  }
376}
377