1/* 2 * Copyright (c) 2024 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 { BusinessError, Callback } from '@ohos.base'; 17import { curves, LengthMetrics, SymbolGlyphModifier, window, AtomicServiceBar } from '@kit.ArkUI'; 18import { bundleManager, common } from '@kit.AbilityKit'; 19import { hilog } from '@kit.PerformanceAnalysisKit'; 20import { i18n } from '@kit.LocalizationKit'; 21 22/** 23 * 背景渐变色相关数据 24 */ 25const backGroundColor: string[] = ['rgb(0,0,0)', 'rgb(255,255,255)', 'rgb(241,243,245)']; 26const backGroundTransparentGradientColor: string[][] = [['rgba(0,0,0,0)', 'rgba(0,0,0,1)'], 27 ['rgba(255,255,255,0)', 'rgba(255,255,255,1)'], ['rgba(241,243,245,0)', 'rgba(241,243,245,1)']]; 28const transparencyMapArray: number[] = [0.15, 0.15, 0.4, 0.6, 0.8]; 29const RECTANGLE_OUTSIDE_OFFSET_ONE = 1; 30const COLOR_RATIO_THIRTY_PERCENT = 0.3; 31const COLOR_RATIO_FIFTY_PERCENT = 0.5; 32const COLOR_RATIO_SEVENTY_PERCENT = 0.7; 33const COLOR_RATIO_FORTY_PERCENT = 0.4; 34const COLOR_RATIO_SIXTY_PERCENT = 0.6; 35const COLOR_RATIO_ONE_FIFTY_PERCENT = 1.5; 36const COORDINATE_NEGATIVE_ONE = -1; 37const BLUR_CONSTANT = 500; 38 39/** 40 * 三种标题栏相关数据 41 */ 42const BREAK_POINT_VP_SM = 600; 43const BREAK_POINT_VP_MD = 840; 44const BREAK_POINT_SM = 'sm'; 45const BREAK_POINT_MD = 'md'; 46const BREAK_POINT_LG = 'lg'; 47const SIDE_BAR_EMBED_MIN_WIDTH = 240; 48const SIDE_BAR_OVERLAY_WIDTH = 304; 49const SIDE_BAR_COMMON_WIDTH = 360; 50const CONTENT_MIN_WIDTH = 600; 51/** 52 * menubar避让区域宽度计算修正时用到的数据 53 */ 54const MENUBAR_X_FIRST_THRESHOLD: number = 200; 55const MENUBAR_X_SECOND_THRESHOLD: number = 40; 56const MENUBAR_CORRECTION_OFFSET_VALUE: number = 92; 57 58/** 59 * 背景颜色的不透明度的枚举类型 60 * 61 * @enum { number }. 62 */ 63export enum GradientAlpha { 64 /** 65 * 不透明度为0.2 66 * 67 */ 68 OPACITY_20 = 1, 69 /** 70 * 不透明度为0.6 71 * 72 */ 73 OPACITY_60 = 2, 74 /** 75 * 不透明度为0.8 76 */ 77 OPACITY_80 = 3, 78 /** 79 * 不透明度为1.0 80 */ 81 OPACITY_100 = 4 82} 83 84/** 85 * 背景颜色融合方式 86 * 87 * @enum { number }. 88 */ 89export enum MixMode { 90 /** 91 * 两种颜色所占比例相同 92 */ 93 AVERAGE = 1, 94 /** 95 * 一种颜色穿过另一种颜色 96 */ 97 CROSS = 2, 98 /** 99 * 一种颜色渐渐转变为另一种颜色 100 */ 101 TOWARDS = 3 102} 103 104/** 105 * 背景底色 106 * 107 * @enum { number }. 108 */ 109export enum BackgroundTheme { 110 /** 111 * 黑色 112 */ 113 DARK = 1, 114 /** 115 * 白色 116 */ 117 LIGHT = 2, 118 /** 119 * 颜色值 #F1F3F5 120 */ 121 DEFAULT = 3 122} 123 124@Component 125export struct AtomicServiceNavigation { 126 @State navPathStack?: NavPathStack = new NavPathStack(); 127 @BuilderParam navigationContent?: Callback<void>; 128 @Prop title?: ResourceStr; 129 @Prop titleOptions?: TitleOptions = { isBlurEnabled: true }; 130 @Prop gradientBackground?: GradientBackground; 131 @Prop hideTitleBar?: boolean; 132 @Prop navBarWidth?: Length; 133 @Prop mode?: NavigationMode; 134 @BuilderParam navDestinationBuilder?: NavDestinationBuilder = this.defaultNavDestinationBuilder; 135 @Prop navBarWidthRange?: [Dimension, Dimension]; 136 @Prop minContentWidth?: Dimension; 137 @Prop sideBarOptions?: SideBarOptions; 138 @BuilderParam sideBarContent?: Callback<void>; 139 @State private showMaskLayer: boolean = false; 140 @State private marginWindowLeft: number = 16; 141 @State private currentBreakPoint: string = BREAK_POINT_SM; 142 @State private sideBarAttribute: sideBarAttributeSet = new sideBarAttributeSet(); 143 @State private controlButtonVisible: boolean = true; 144 @State private titleBuilderPaddingEndWidth: number = 0; 145 @BuilderParam menus?: CustomBuilder | Array<NavigationMenuItem>; 146 stateChangeCallback?: Callback<boolean>; 147 modeChangeCallback?: Callback<NavigationMode>; 148 149 private settings: RenderingContextSettings = new RenderingContextSettings(true); 150 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); 151 @State private navigationWidth: number = 0; 152 @State private navigationHeight: number = 0; 153 154 private mainWindow?: window.Window; 155 private onWindowSizeChangeCallback?: Callback<Size>; 156 private onAvoidSafeAreaChangeCallback?: Callback<window.AvoidAreaOptions>; 157 private sideBarHelper?: SideBarHelper; 158 private atomicServiceIcon?: PixelMap; 159 private navigationBarVisibility: boolean = true; 160 161 @Builder 162 defaultNavDestinationBuilder(name: string, param?: Object) { 163 } 164 165 @Builder 166 BackgroundBuilder(gradientBackground: GradientBackground) { 167 Canvas(this.context) 168 .opacity(transparencyMapArray[(gradientBackground.alpha === undefined) ? GradientAlpha.OPACITY_20 : 169 gradientBackground.alpha]) 170 .backdropBlur(BLUR_CONSTANT) 171 .height(this.navigationHeight) 172 .backgroundColor(gradientBackground.backgroundTheme === undefined ? backGroundColor[2] : 173 backGroundColor[gradientBackground.backgroundTheme - 1]) 174 .onReady(() => { 175 if (gradientBackground.secondaryColor === undefined) { 176 //单色渐变 177 this.drawSingleGradient(this.context, gradientBackground.primaryColor, 178 gradientBackground.backgroundTheme === undefined ? backGroundColor[2] : 179 backGroundColor[gradientBackground.backgroundTheme - 1]); 180 } else { 181 if (gradientBackground.mixMode === MixMode.AVERAGE) { 182 //双色渐变五五分 183 this.drawGradientCanvasHalf(this.context, gradientBackground.primaryColor, 184 gradientBackground.secondaryColor); 185 } else if (gradientBackground.mixMode === MixMode.CROSS) { 186 //第一种双色渐变三七分 187 this.drawGradientCanvasCross(this.context, gradientBackground.primaryColor, 188 gradientBackground.secondaryColor); 189 } else { 190 //第二种双色渐变三七分 191 this.drawGradientCanvasTowards(this.context, gradientBackground.primaryColor, 192 gradientBackground.secondaryColor); 193 } 194 this.drawTransparentGradient(this.context, 195 gradientBackground.backgroundTheme === undefined ? BackgroundTheme.DEFAULT : 196 gradientBackground.backgroundTheme); 197 } 198 }).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 199 } 200 201 aboutToAppear(): void { 202 this.initWindow(); 203 this.initIcon(); 204 this.initSideBarAttr(); 205 } 206 207 /** 208 * 初始化侧边栏相关信息 209 */ 210 private initSideBarAttr(): void { 211 if (this.titleOptions?.titleBarType !== TitleBarType.DRAWER) { 212 return; 213 } 214 this.sideBarAttribute = new sideBarAttributeSet(); 215 this.sideBarHelper = new SideBarHelper(); 216 let sideBarStatusListener = (show: boolean) => { 217 this.sideBarAttribute.showSideBar = show; 218 this.updateControlButtonVisibility(); 219 if (this.sideBarOptions?.onChange) { 220 this.sideBarOptions.onChange(show); 221 } 222 }; 223 this.sideBarHelper.registerListener(sideBarStatusListener); 224 } 225 226 /** 227 * 窗口初始化或者尺寸发生变化时,让menubar避让宽度更新 228 */ 229 private freshMenubarAvoidAreaWidth(mainWindow: window.Window): void { 230 setTimeout(() => { 231 const atomicServiceBar: Nullable<AtomicServiceBar> = this.getUIContext().getAtomicServiceBar(); 232 if (!atomicServiceBar) { 233 this.titleBuilderPaddingEndWidth = 0; 234 return; 235 } 236 let menubarX: number = atomicServiceBar.getBarRect().x; 237 let corretionWidth: number = 0; 238 if (menubarX > MENUBAR_X_FIRST_THRESHOLD) { 239 const mainWindowWidth: number = px2vp(mainWindow.getWindowProperties()?.windowRect?.width) - menubarX; 240 corretionWidth = mainWindowWidth > MENUBAR_X_FIRST_THRESHOLD ? 0 : mainWindowWidth; 241 } else if (menubarX < MENUBAR_X_SECOND_THRESHOLD) { 242 corretionWidth = menubarX + MENUBAR_CORRECTION_OFFSET_VALUE; 243 } 244 let currentWidth: number = atomicServiceBar.getBarRect().width; 245 this.titleBuilderPaddingEndWidth = corretionWidth > currentWidth ? corretionWidth : currentWidth; 246 }, 100); 247 } 248 249 /** 250 * 初始化window,并设置windowSizeChange监听,更新断点信息 251 */ 252 private initWindow(): void { 253 let context = getContext(this) as common.UIAbilityContext; 254 context?.windowStage?.getMainWindow().then(mainWindow => { 255 if (!mainWindow) { 256 return; 257 } 258 this.mainWindow = mainWindow; 259 if (this.titleOptions?.titleBarType === TitleBarType.DRAWER) { 260 this.sideBarHelper?.updateLayout(this.currentBreakPoint, this.sideBarAttribute); 261 } 262 this.updateBreakPoint(mainWindow.getWindowProperties()?.windowRect?.width); 263 this.freshMenubarAvoidAreaWidth(mainWindow); 264 this.onWindowSizeChangeCallback = ((windowSize) => { 265 this.updateBreakPoint(windowSize?.width); 266 this.freshMenubarAvoidAreaWidth(mainWindow); 267 }); 268 mainWindow.on('windowSizeChange', this.onWindowSizeChangeCallback); 269 }).catch((err: Error) => { 270 console.error(`AtomicServiceNavigation get main window failed, message is ${err?.message}`); 271 }); 272 } 273 274 /** 275 * 初始化icon,用户如果设置了则用自定义的icon,没有设置则用元服务图标 276 */ 277 private initIcon(): void { 278 if ((this.titleOptions?.titleBarType !== TitleBarType.ROUND_ICON && 279 this.titleOptions?.titleBarType !== TitleBarType.SQUARED_ICON) || this.titleOptions?.titleIcon) { 280 return; 281 } 282 let bundleFlags: number = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION; 283 let bundleInfo: bundleManager.BundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags); 284 let iconRes: Resource = bundleInfo?.appInfo?.iconResource; 285 this.atomicServiceIcon = getContext(this)?.resourceManager?.getDrawableDescriptor(iconRes)?.getPixelMap(); 286 } 287 288 /** 289 * 更新断点信息 290 */ 291 private updateBreakPoint(windowWidth: number): void { 292 if (!windowWidth || windowWidth <= 0) { 293 return; 294 } 295 let widthVp = px2vp(windowWidth); 296 let newBreakPoint: string = ''; 297 if (widthVp < BREAK_POINT_VP_SM) { 298 newBreakPoint = BREAK_POINT_SM; 299 } else if (widthVp < BREAK_POINT_VP_MD) { 300 newBreakPoint = BREAK_POINT_MD; 301 } else { 302 newBreakPoint = BREAK_POINT_LG; 303 } 304 if (this.currentBreakPoint !== newBreakPoint) { 305 this.currentBreakPoint = newBreakPoint; 306 this.updateMargin(); 307 if (this.titleOptions?.titleBarType === TitleBarType.DRAWER) { 308 this.sideBarHelper?.updateLayout(this.currentBreakPoint, this.sideBarAttribute); 309 } 310 } 311 } 312 313 /** 314 * 更新边距 315 */ 316 private updateMargin(): void { 317 switch (this.currentBreakPoint) { 318 case BREAK_POINT_MD: 319 this.marginWindowLeft = 24; 320 break; 321 case BREAK_POINT_LG: 322 this.marginWindowLeft = 32; 323 break; 324 case BREAK_POINT_SM: 325 default: 326 this.marginWindowLeft = 16; 327 break; 328 } 329 } 330 331 aboutToDisappear(): void { 332 if (this.onWindowSizeChangeCallback) { 333 this.mainWindow?.off('windowSizeChange', this.onWindowSizeChangeCallback); 334 } 335 } 336 337 /** 338 * 更新control button的可见性 339 */ 340 updateControlButtonVisibility(): void { 341 if (this.titleOptions?.titleBarType !== TitleBarType.DRAWER) { 342 return; 343 } 344 345 // 如果侧边栏显示,那么控制图标一定需要显示 346 if (this.sideBarAttribute.showSideBar && this.controlButtonVisible) { 347 return; 348 } 349 350 if (this.currentBreakPoint === BREAK_POINT_LG) { 351 if (this.sideBarAttribute.showSideBar) { 352 if (!this.controlButtonVisible) { 353 this.controlButtonVisible = true; 354 } 355 return; 356 } 357 358 if (!this.navigationBarVisibility && !this.sideBarAttribute.showSideBar) { 359 if (this.controlButtonVisible) { 360 this.controlButtonVisible = false; 361 } 362 } else { 363 if (!this.controlButtonVisible) { 364 this.controlButtonVisible = true; 365 } 366 } 367 } else { 368 if (this.controlButtonVisible !== this.navigationBarVisibility) { 369 this.controlButtonVisible = this.navigationBarVisibility; 370 } 371 } 372 } 373 374 @Builder 375 drawerTitleBuilder() { 376 if (this.titleOptions?.titleBarType === TitleBarType.DRAWER && this.title) { 377 Row() { 378 Text(this.title) 379 .maxLines(2) 380 .minFontSize(14) 381 .maxFontSize(26) 382 .height(36) 383 .fontColor($r('sys.color.font_primary')) 384 .textOverflow({ overflow: TextOverflow.Ellipsis }) 385 .fontWeight(FontWeight.Bold) 386 .width(0) 387 .layoutWeight(1) 388 .clip(true) 389 .textAlign(i18n.isRTL(i18n.System.getSystemLocale()) ? TextAlign.End : TextAlign.Start) 390 } 391 .padding({ 392 start: LengthMetrics.vp(this.currentBreakPoint !== BREAK_POINT_LG ? this.marginWindowLeft + 36 + 8 : 393 (this.sideBarAttribute.showSideBar ? 8 : this.marginWindowLeft + 36 + 8)), 394 top: LengthMetrics.vp(10), 395 bottom: LengthMetrics.vp(10), 396 // 在Stack模式,或者非分栏模式下右侧需要有一定padding值,避免超长文本时不能避让menuBar 397 end: ((this.currentBreakPoint === BREAK_POINT_SM && 398 (this.mode === NavigationMode.Auto || !this.mode)) || this.mode === NavigationMode.Stack) ? 399 LengthMetrics.vp(this.titleBuilderPaddingEndWidth) : LengthMetrics.vp(0) 400 }) 401 .width('100%') 402 } 403 } 404 405 @Builder 406 maskLayer() { 407 Column() { 408 } 409 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 410 .backgroundColor($r('sys.color.ohos_id_color_mask_thin')) 411 .transition(TransitionEffect.opacity(0).animation({ duration: 500, curve: Curve.Linear })) 412 .width('100%') 413 .height('100%') 414 .accessibilityLevel('no') 415 .onClick(() => { 416 this.changeSideBarWithAnimation(false); 417 }) 418 } 419 420 @Styles 421 controlButtonStyle() { 422 .backgroundColor($r('sys.color.ohos_id_color_button_normal')) 423 .width(36) 424 .height(36) 425 .borderRadius(18) 426 .margin({ 427 start: LengthMetrics.vp(this.marginWindowLeft), 428 }) 429 .zIndex(2) 430 .draggable(false) 431 .onClick(() => { 432 const isShowSideBar = !this.sideBarHelper?.isShowSideBar(); 433 this.changeSideBarWithAnimation(isShowSideBar); 434 }) 435 .position({ 436 start: LengthMetrics.vp(0), 437 top: LengthMetrics.vp(8) 438 }) 439 .visibility(this.controlButtonVisible ? Visibility.Visible : Visibility.None) 440 } 441 442 @Builder 443 controlButton() { 444 if (this.sideBarAttribute.showSideBar) { 445 if (!this.sideBarOptions?.sideBarIcon) { 446 Button({ type: ButtonType.Circle }) { 447 SymbolGlyph($r('sys.symbol.open_sidebar')) 448 .fontWeight(FontWeight.Normal) 449 .fontSize($r('sys.float.ohos_id_text_size_headline7')) 450 .fontColor([$r('sys.color.ohos_id_color_titlebar_icon')]) 451 } 452 .controlButtonStyle() 453 } else if (this.sideBarOptions?.sideBarIcon instanceof SymbolGlyphModifier) { 454 Button({ type: ButtonType.Circle }) { 455 SymbolGlyph() 456 .attributeModifier(this.sideBarOptions?.sideBarIcon) 457 .fontSize($r('sys.float.ohos_id_text_size_headline7')) 458 .size({ 459 width: $r('sys.float.ohos_id_text_size_headline7'), 460 height: $r('sys.float.ohos_id_text_size_headline7') 461 }) 462 } 463 .controlButtonStyle() 464 } else { 465 Image(this.sideBarOptions?.sideBarIcon) 466 .controlButtonStyle() 467 } 468 } else { 469 if (!this.titleOptions?.titleIcon) { 470 Button({ type: ButtonType.Circle }) { 471 SymbolGlyph($r('sys.symbol.close_sidebar')) 472 .fontWeight(FontWeight.Normal) 473 .fontSize($r('sys.float.ohos_id_text_size_headline7')) 474 .fontColor([$r('sys.color.ohos_id_color_titlebar_icon')]) 475 } 476 .controlButtonStyle() 477 } else if (this.titleOptions?.titleIcon instanceof SymbolGlyphModifier) { 478 Button({ type: ButtonType.Circle }) { 479 SymbolGlyph() 480 .attributeModifier(this.titleOptions?.titleIcon) 481 .fontSize($r('sys.float.ohos_id_text_size_headline7')) 482 .size({ 483 width: $r('sys.float.ohos_id_text_size_headline7'), 484 height: $r('sys.float.ohos_id_text_size_headline7') 485 }) 486 } 487 .controlButtonStyle() 488 } else { 489 Image(this.titleOptions?.titleIcon) 490 .controlButtonStyle() 491 } 492 } 493 } 494 495 @Builder 496 sideBar() { 497 Row() { 498 if (this.sideBarContent) { 499 this.sideBarContent(); 500 } 501 } 502 .border({ width: { 503 left: 0, 504 right: '1px', 505 top: 0, 506 bottom: 0 507 }, 508 color: $r('sys.color.comp_divider'), 509 style: { 510 right: BorderStyle.Solid 511 } 512 }) 513 .alignItems(VerticalAlign.Top) 514 .width('100%') 515 .height('100%') 516 .padding({ top: 56 }) 517 .focusable(true) 518 .defaultFocus(true) 519 .backgroundColor(this.sideBarOptions?.sideBarBackground ?? $r('sys.color.ohos_id_color_sub_background')) 520 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 521 } 522 523 @Styles 524 roundIconStyle() { 525 .backgroundColor('rgba(0, 0, 0, 0)') 526 .width(36) 527 .height(36) 528 .borderRadius(18) 529 .margin({ 530 start: LengthMetrics.vp(this.marginWindowLeft), 531 end: LengthMetrics.vp(8), 532 top: LengthMetrics.vp(10), 533 bottom: LengthMetrics.vp(10) 534 }) 535 .zIndex(2) 536 .draggable(false) 537 .position({ 538 start: LengthMetrics.vp(0), 539 top: LengthMetrics.vp(0) 540 }) 541 } 542 543 @Builder 544 roundIconBuilder() { 545 if (this.titleOptions?.titleIcon instanceof SymbolGlyphModifier) { 546 Button({ type: ButtonType.Circle }) { 547 SymbolGlyph() 548 .attributeModifier(this.titleOptions?.titleIcon) 549 .fontSize($r('sys.float.ohos_id_text_size_headline7')) 550 .size({ 551 width: $r('sys.float.ohos_id_text_size_headline7'), 552 height: $r('sys.float.ohos_id_text_size_headline7') 553 }) 554 } 555 .stateEffect(false) 556 .roundIconStyle() 557 } else { 558 Image(this.titleOptions?.titleIcon ?? this.atomicServiceIcon) 559 .roundIconStyle() 560 } 561 } 562 563 @Styles 564 longIconStyles() { 565 .height(36) 566 .width(36) 567 .margin({ 568 start: LengthMetrics.vp(this.marginWindowLeft), 569 end: LengthMetrics.vp(-12), 570 top: LengthMetrics.vp(10), 571 bottom: LengthMetrics.vp(10) 572 }) 573 .draggable(false) 574 .position({ 575 start: LengthMetrics.vp(0), 576 top: LengthMetrics.vp(0) 577 }) 578 .backgroundColor('rgba(0, 0, 0, 0)') 579 } 580 581 @Builder 582 longIconBuilder() { 583 if (this.titleOptions?.titleIcon instanceof SymbolGlyphModifier) { 584 Button() { 585 SymbolGlyph() 586 .attributeModifier(this.titleOptions?.titleIcon) 587 .fontSize($r('sys.float.ohos_id_text_size_headline7')) 588 .size({ 589 width: $r('sys.float.ohos_id_text_size_headline7'), 590 height: $r('sys.float.ohos_id_text_size_headline7') 591 }) 592 } 593 .longIconStyles() 594 .stateEffect(false) 595 .type(ButtonType.Normal) 596 } else { 597 Image(this.titleOptions?.titleIcon ?? this.atomicServiceIcon) 598 .longIconStyles() 599 .objectFit(ImageFit.Auto) 600 } 601 } 602 603 @Builder 604 titleBuilder() { 605 if (this.titleOptions?.titleBarType === TitleBarType.SQUARED_ICON) { 606 this.longIconBuilder(); 607 } else { 608 this.roundIconBuilder(); 609 } 610 } 611 612 /** 613 * 根据当前屏幕尺寸判断是否要显示用户插入的布局 614 */ 615 private isShowMenus(): boolean { 616 return this.mode === NavigationMode.Stack && this.currentBreakPoint !== BREAK_POINT_SM; 617 } 618 619 /** 620 * 根据用户传入的类型和当前屏幕尺寸判断是否要显示NavigationMenuItem列表 621 */ 622 private getMenuItemArray(): undefined | Array<NavigationMenuItem> { 623 return this.isShowMenus() && this.menus instanceof Array ? this.menus : undefined; 624 } 625 626 build() { 627 Stack() { 628 Column() { 629 if (this.gradientBackground !== undefined) { 630 this.BackgroundBuilder({ 631 primaryColor: this.gradientBackground.primaryColor, 632 secondaryColor: this.gradientBackground.secondaryColor, 633 backgroundTheme: this.gradientBackground.backgroundTheme, 634 mixMode: this.gradientBackground.mixMode, 635 alpha: this.gradientBackground.alpha 636 }) 637 } 638 } 639 if (this.titleOptions?.titleBarType === TitleBarType.DRAWER) { 640 this.controlButton(); 641 642 // 断点为LG时,侧边栏嵌入到组件内;断点为MD或SM时,侧边栏悬浮在B栏上 643 SideBarContainer(this.currentBreakPoint === BREAK_POINT_LG ? SideBarContainerType.Embed : 644 SideBarContainerType.Overlay) { 645 this.sideBar() 646 647 Stack() { 648 Navigation(this.navPathStack) { 649 if (this.navigationContent) { 650 this.navigationContent(); 651 } 652 } 653 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 654 .title(this.drawerTitleBuilder(), this.getTitleOption()) 655 .menus(this.isShowMenus() && this.menus instanceof Function ? this.menus() : this.getMenuItemArray()) 656 .titleMode(NavigationTitleMode.Mini) 657 .hideBackButton(true) 658 .hideTitleBar(this.hideTitleBar) 659 .navBarWidth(this.navBarWidth) 660 .navBarPosition(NavBarPosition.Start) 661 .mode(this.mode) 662 .navDestination(this.navDestinationBuilder) 663 .navBarWidthRange(this.navBarWidthRange) 664 .minContentWidth(this.minContentWidth) 665 .onNavigationModeChange(this.modeChangeCallback) 666 .onNavBarStateChange((visible: boolean) => { 667 this.navigationBarVisibility = visible; 668 this.updateControlButtonVisibility(); 669 if (this.stateChangeCallback) { 670 this.stateChangeCallback(visible); 671 } 672 }) 673 674 if (this.titleOptions?.titleBarType === TitleBarType.DRAWER && this.sideBarAttribute.showSideBar && 675 this.currentBreakPoint !== BREAK_POINT_LG && this.showMaskLayer) { 676 this.maskLayer(); 677 } 678 } 679 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 680 } 681 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 682 .sideBarWidth(this.sideBarAttribute.sideBarWidth) 683 .minContentWidth(this.sideBarAttribute.minContentWidthOfSideBar) 684 .minSideBarWidth(this.sideBarAttribute.minSideBarWidth) 685 .maxSideBarWidth(this.sideBarAttribute.maxSideBarWidth) 686 .showControlButton(false) 687 .showSideBar(this.sideBarAttribute.showSideBar) 688 .onChange((showSideBar: boolean) => { 689 if (this.sideBarHelper?.isShowSideBar() !== showSideBar) { 690 this.changeSideBarWithAnimation(showSideBar); 691 } 692 }) 693 } else { 694 Navigation(this.navPathStack) { 695 if (this.navigationContent) { 696 this.navigationContent(); 697 } 698 } 699 .title((this.titleOptions?.titleIcon || (this.titleOptions?.titleBarType && !this.title)) ? this.titleBuilder() : 700 this.title, 701 this.getTitleOption()) 702 .titleMode(NavigationTitleMode.Mini) 703 .menus(this.isShowMenus() && this.menus instanceof Function ? this.menus() : this.getMenuItemArray()) 704 .hideBackButton(true) 705 .hideTitleBar(this.hideTitleBar) 706 .navBarWidth(this.navBarWidth) 707 .navBarPosition(NavBarPosition.Start) 708 .mode(this.mode) 709 .navDestination(this.navDestinationBuilder) 710 .navBarWidthRange(this.navBarWidthRange) 711 .minContentWidth(this.minContentWidth) 712 .onNavBarStateChange(this.stateChangeCallback) 713 .onNavigationModeChange(this.modeChangeCallback) 714 } 715 } 716 .align(Alignment.TopStart) 717 .width('100%') 718 .height('100%') 719 .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => { 720 this.navigationWidth = newValue.width as number; 721 this.navigationHeight = newValue.height as number; 722 }) 723 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 724 } 725 726 /** 727 * 双色渐变下两种颜色各占50%的实现,把整个画布区域分为两个一样的矩形在绘制 728 * @param context 画布上下文 729 * @param primaryColor 第一种颜色 730 * @param secondaryColor 第二种颜色 731 */ 732 drawGradientCanvasHalf(context: CanvasRenderingContext2D, primaryColor: ResourceColor, 733 secondaryColor: ResourceColor): void { 734 let height = this.navigationHeight * COLOR_RATIO_THIRTY_PERCENT; 735 let grad1 = 736 context.createLinearGradient(COORDINATE_NEGATIVE_ONE * this.navigationWidth * COLOR_RATIO_FIFTY_PERCENT, 737 height, this.navigationWidth * COLOR_RATIO_FIFTY_PERCENT, 0); 738 let grad2 = context.createLinearGradient(this.navigationWidth * COLOR_RATIO_ONE_FIFTY_PERCENT, height, 739 this.navigationWidth * COLOR_RATIO_FIFTY_PERCENT, 0); 740 grad1.addColorStop(0, this.resourceColorToString(primaryColor)); 741 grad1.addColorStop(COLOR_RATIO_FIFTY_PERCENT, this.resourceColorToString(primaryColor)); 742 grad1.addColorStop(1, this.resourceColorToString(secondaryColor)); 743 grad2.addColorStop(0, this.resourceColorToString(primaryColor)); 744 grad2.addColorStop(COLOR_RATIO_FIFTY_PERCENT, this.resourceColorToString(primaryColor)); 745 grad2.addColorStop(1, this.resourceColorToString(secondaryColor)); 746 context.fillStyle = grad1; 747 context.fillRect(0, 0, this.navigationWidth * COLOR_RATIO_FIFTY_PERCENT, height); 748 context.fillStyle = grad2; 749 context.fillRect(this.navigationWidth * COLOR_RATIO_FIFTY_PERCENT, 0, this.navigationWidth, height); 750 } 751 752 /** 753 * 双色渐变的一种实现,把画布先分为两个大矩形,再把其中一个矩形分为两个小矩形 754 * @param context 画布上下文 755 * @param primaryColor 第一种颜色 756 * @param secondaryColor 第二种颜色 757 */ 758 drawGradientCanvasCross(context: CanvasRenderingContext2D, primaryColor: ResourceColor, 759 secondaryColor: ResourceColor): void { 760 let height = this.navigationHeight * COLOR_RATIO_THIRTY_PERCENT; 761 let grad1 = context.createLinearGradient(0, 0, COLOR_RATIO_SEVENTY_PERCENT * this.navigationWidth, 0); 762 grad1.addColorStop(0, this.resourceColorToString(primaryColor)); 763 grad1.addColorStop(COLOR_RATIO_FIFTY_PERCENT, this.resourceColorToString(primaryColor)); 764 grad1.addColorStop(1, this.resourceColorToString(secondaryColor)); 765 context.fillStyle = grad1; 766 context.fillRect(0, 0, COLOR_RATIO_SEVENTY_PERCENT * this.navigationWidth, height); 767 let y1 = (COLOR_RATIO_FIFTY_PERCENT * height - COLOR_RATIO_THIRTY_PERCENT * this.navigationWidth) > 0 ? 768 COLOR_RATIO_FIFTY_PERCENT * height - COLOR_RATIO_THIRTY_PERCENT * this.navigationWidth : 0; 769 let grad2 = 770 context.createLinearGradient(COLOR_RATIO_SEVENTY_PERCENT * this.navigationWidth, y1, this.navigationWidth, 771 height * COLOR_RATIO_FIFTY_PERCENT); 772 grad2.addColorStop(0, this.resourceColorToString(secondaryColor)); 773 grad2.addColorStop(COLOR_RATIO_FORTY_PERCENT, this.resourceColorToString(secondaryColor)); 774 grad2.addColorStop(1, this.resourceColorToString(primaryColor)); 775 context.fillStyle = grad2; 776 context.fillRect(COLOR_RATIO_SEVENTY_PERCENT * this.navigationWidth - RECTANGLE_OUTSIDE_OFFSET_ONE, 0, 777 this.navigationWidth * COLOR_RATIO_THIRTY_PERCENT + RECTANGLE_OUTSIDE_OFFSET_ONE, 778 height * COLOR_RATIO_FIFTY_PERCENT + RECTANGLE_OUTSIDE_OFFSET_ONE); 779 let y2 = (COLOR_RATIO_FIFTY_PERCENT * height - COLOR_RATIO_THIRTY_PERCENT * this.navigationWidth) > 0 ? 780 COLOR_RATIO_FIFTY_PERCENT * height + COLOR_RATIO_THIRTY_PERCENT * this.navigationWidth : height; 781 let grad3 = 782 context.createLinearGradient(COLOR_RATIO_SEVENTY_PERCENT * this.navigationWidth, y2, this.navigationWidth, 783 height * COLOR_RATIO_FIFTY_PERCENT); 784 grad3.addColorStop(0, this.resourceColorToString(secondaryColor)); 785 grad3.addColorStop(COLOR_RATIO_FORTY_PERCENT, this.resourceColorToString(secondaryColor)); 786 grad3.addColorStop(1, this.resourceColorToString(primaryColor)); 787 context.fillStyle = grad3; 788 context.fillRect(COLOR_RATIO_SEVENTY_PERCENT * this.navigationWidth - RECTANGLE_OUTSIDE_OFFSET_ONE, 789 height * COLOR_RATIO_FIFTY_PERCENT, 790 COLOR_RATIO_THIRTY_PERCENT * this.navigationWidth + RECTANGLE_OUTSIDE_OFFSET_ONE, 791 height * COLOR_RATIO_FIFTY_PERCENT); 792 } 793 794 /** 795 * 双色渐变的一种实现,从矩形左上角颜色渐变到右下角 796 * @param context 画布上下文 797 * @param primaryColor 第一种颜色 798 * @param secondaryColor 第二种颜色 799 */ 800 drawGradientCanvasTowards(context: CanvasRenderingContext2D, primaryColor: ResourceColor, 801 secondaryColor: ResourceColor): void { 802 let height = this.navigationHeight * COLOR_RATIO_THIRTY_PERCENT; 803 let grad = context.createLinearGradient(0, 0, this.navigationWidth, height); 804 grad.addColorStop(0, this.resourceColorToString(primaryColor)); 805 grad.addColorStop(COLOR_RATIO_FORTY_PERCENT, this.resourceColorToString(primaryColor)); 806 grad.addColorStop(1, this.resourceColorToString(secondaryColor)); 807 context.fillStyle = grad; 808 context.fillRect(0, 0, this.navigationWidth, height); 809 } 810 811 /** 812 * 双色渐变下透明效果的实现 813 * @param context 画布上下文 814 * @param backgroundTheme 背景色底色 815 */ 816 drawTransparentGradient(context: CanvasRenderingContext2D, backgroundTheme: BackgroundTheme): void { 817 let height = this.navigationHeight * COLOR_RATIO_THIRTY_PERCENT; 818 let grad = context.createLinearGradient(0, 0, 0, height); 819 grad.addColorStop(0, backGroundTransparentGradientColor[backgroundTheme - 1][0]); 820 grad.addColorStop(1, backGroundTransparentGradientColor[backgroundTheme - 1][1]); 821 context.fillStyle = grad; 822 context.fillRect(0, 0, this.navigationWidth + RECTANGLE_OUTSIDE_OFFSET_ONE, height + 823 RECTANGLE_OUTSIDE_OFFSET_ONE); 824 if (backgroundTheme === BackgroundTheme.DARK) { 825 context.fillStyle = Color.Black; 826 context.fillRect(0, height, this.navigationWidth, this.navigationHeight - height); 827 } 828 } 829 830 /** 831 * 单色渐变: 832 * @param context 画布上下文 833 * @param primaryColor createLinearGradient初始颜色为primaryColor,结束颜色为底色 834 * @param backgroundColor 颜色线性渐变的结束颜色 835 */ 836 drawSingleGradient(context: CanvasRenderingContext2D, 837 primaryColor: ResourceColor, backgroundColor: string): void { 838 let height = this.navigationHeight * COLOR_RATIO_SIXTY_PERCENT; 839 let grad1 = context.createLinearGradient(0, 0, 0, height); 840 grad1.addColorStop(0, this.resourceColorToString(primaryColor)); 841 grad1.addColorStop(1, backgroundColor); 842 context.fillStyle = grad1; 843 context.fillRect(0, 0, this.navigationWidth, height); 844 //当背景为黑色的时候需要特殊处理下 845 if (backgroundColor === backGroundColor[0]) { 846 context.fillStyle = Color.Black 847 context.fillRect(0, height, this.navigationWidth, this.navigationHeight * (1 - COLOR_RATIO_SIXTY_PERCENT)); 848 } 849 } 850 851 /** 852 * ResourceColor转化为能作为addColorStop使用的字符串 853 * @param resource ResourceColor = Color | number | string | Resource,对于Resource转化为直接使用的字符串需要特殊处理 854 * @returns 855 */ 856 resourceColorToString(resource: ResourceColor): string { 857 if (typeof resource === 'object') { 858 try { 859 return getContext(this).resourceManager.getStringSync(resource) 860 } catch (error) { 861 let code = (error as BusinessError).code; 862 let message = (error as BusinessError).message; 863 hilog.error(0x0000, 'AtomicServiceNavigation', 864 `resourceColorToString - systemResourceManager getStringValue failed, error code: ${code}, message: ${message}.`); 865 } 866 return ''; 867 } else { 868 return resource.toString(); 869 } 870 } 871 872 /** 873 * 获取NavigationTitleOptions 874 */ 875 private getTitleOption(): NavigationTitleOptions { 876 return { 877 backgroundColor: this.titleOptions?.backgroundColor, 878 backgroundBlurStyle: this.titleOptions?.isBlurEnabled ? BlurStyle.COMPONENT_THICK : BlurStyle.NONE, 879 barStyle: this.titleOptions?.barStyle 880 }; 881 } 882 883 /** 884 * 更新control button的可见性,并运行动画效果 885 */ 886 private changeSideBarWithAnimation(isShowSidebar: boolean): void { 887 animateTo({ 888 duration: 500, 889 curve: curves.cubicBezierCurve(0.2, 0.2, 0.1, 1), 890 onFinish: () => { 891 this.showMaskLayer = isShowSidebar; 892 } 893 }, () => { 894 if (this.sideBarHelper) { 895 this.sideBarHelper.setShowSideBar(isShowSidebar); 896 } 897 this.showMaskLayer = true; 898 }) 899 } 900} 901 902/** 903 * 侧边栏相关参数 904 */ 905@Observed 906class sideBarAttributeSet { 907 /** 908 * 侧边栏宽度 909 */ 910 public sideBarWidth: number = SIDE_BAR_OVERLAY_WIDTH; 911 /** 912 * 侧边栏最小宽度 913 */ 914 public minSideBarWidth = SIDE_BAR_OVERLAY_WIDTH; 915 /** 916 * 侧边栏最大宽度 917 */ 918 public maxSideBarWidth = SIDE_BAR_OVERLAY_WIDTH; 919 /** 920 * 侧边栏内容最小宽度 921 */ 922 public minContentWidthOfSideBar: Dimension = SIDE_BAR_COMMON_WIDTH; 923 /** 924 * 侧边栏显示隐藏状态 925 */ 926 public showSideBar: boolean = false; 927} 928 929/** 930 * 侧边栏对外暴露相关参数 931 */ 932export interface SideBarOptions { 933 /** 934 * 侧边栏背景 935 */ 936 sideBarBackground?: ResourceColor; 937 938 /** 939 * 侧边栏显示隐藏状态变化监听 940 */ 941 onChange?: (show: boolean) => void; 942 943 /** 944 * 侧边栏icon 945 */ 946 sideBarIcon?: Resource | SymbolGlyphModifier; 947} 948 949/** 950 * 标题相关参数 951 */ 952export interface TitleOptions { 953 /** 954 * 背景色 955 */ 956 backgroundColor?: ResourceColor, 957 958 /** 959 * 是否启动模糊效果 960 */ 961 isBlurEnabled?: boolean, 962 963 /** 964 * 标题栏风格 965 */ 966 barStyle?: BarStyle, 967 968 /** 969 * 标题栏类型 970 */ 971 titleBarType?: TitleBarType, 972 973 /** 974 * 标题栏icon 975 */ 976 titleIcon?: Resource | SymbolGlyphModifier; 977} 978 979export type NavDestinationBuilder = (name: string, param?: Object) => void; 980 981export interface GradientBackground { 982 primaryColor: ResourceColor, 983 secondaryColor?: ResourceColor, 984 backgroundTheme?: BackgroundTheme, 985 mixMode?: MixMode, 986 alpha?: GradientAlpha 987} 988 989/** 990 * 侧边栏辅助管理类 991 */ 992class SideBarHelper { 993 private isExpandSideBar: boolean = false; 994 private listener?: Function; 995 996 /** 997 * 注册侧边栏显隐状态变化监听 998 * 999 * @param listener 监听器对象 1000 */ 1001 public registerListener(listener: Function): void { 1002 this.listener = listener; 1003 } 1004 1005 /** 1006 * 取消注册监听 1007 */ 1008 public unregisterListener(): void { 1009 this.listener = undefined; 1010 } 1011 1012 /** 1013 * 获取侧边栏显示隐藏状态 1014 * 1015 * @returns 侧边栏是否显示 1016 */ 1017 public isShowSideBar(): boolean { 1018 return this.isExpandSideBar; 1019 } 1020 1021 /** 1022 * 设置侧边栏显示隐藏状态 1023 * 1024 * @param value 显示或隐藏状态 1025 */ 1026 public setShowSideBar(value: boolean): void { 1027 this.isExpandSideBar = value; 1028 if (this.listener) { 1029 this.listener(value); 1030 } 1031 } 1032 1033 /** 1034 * 更新侧边栏布局 1035 * 1036 * @param breakPoint 当前断点 1037 * @param layout 布局参数 1038 */ 1039 public updateLayout(breakPoint: string, layout: sideBarAttributeSet): void { 1040 if (breakPoint === BREAK_POINT_LG) { 1041 this.updateLGLayout(layout); 1042 } else { 1043 this.updateCommonLayout(layout); 1044 } 1045 } 1046 1047 /** 1048 * 更新除LG外窗口模式的布局 1049 */ 1050 private updateCommonLayout(layout: sideBarAttributeSet): void { 1051 layout.sideBarWidth = SIDE_BAR_OVERLAY_WIDTH; 1052 layout.minContentWidthOfSideBar = '100%'; 1053 } 1054 1055 /** 1056 * 更新LG窗口模式布局 1057 */ 1058 private updateLGLayout(layout: sideBarAttributeSet): void { 1059 layout.sideBarWidth = SIDE_BAR_EMBED_MIN_WIDTH; 1060 layout.minContentWidthOfSideBar = CONTENT_MIN_WIDTH; 1061 } 1062} 1063 1064/** 1065 * 标题栏类型 1066 */ 1067export enum TitleBarType { 1068 /** 1069 * 长图标类型标题栏 1070 */ 1071 SQUARED_ICON = 1, 1072 1073 /** 1074 * 圆形图标类型标题栏 1075 */ 1076 ROUND_ICON, 1077 1078 /** 1079 * 抽屉类型标题栏 1080 */ 1081 DRAWER 1082}