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