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