1/* 2 * Copyright (c) 2023-2023 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 { KeyCode } from '@ohos.multimodalInput.keyCode' 17import MeasureText from '@ohos.measure' 18import window from '@ohos.window' 19import common from '@ohos.app.ability.common' 20import { BusinessError } from '@kit.BasicServicesKit' 21import { hilog } from '@kit.PerformanceAnalysisKit' 22import { SymbolGlyphModifier } from '@kit.ArkUI' 23 24export interface TabTitleBarMenuItem { 25 value: ResourceStr; 26 symbolStyle?: SymbolGlyphModifier; 27 isEnabled?: boolean; 28 action?: () => void; 29 label?: ResourceStr; 30 accessibilityText?: ResourceStr; 31 accessibilityLevel?: string; 32 accessibilityDescription?: ResourceStr; 33} 34 35export interface TabTitleBarTabItem { 36 title: ResourceStr; 37 icon?: ResourceStr; 38 symbolStyle?: SymbolGlyphModifier; 39} 40 41const PUBLIC_MORE = $r('sys.symbol.dot_grid_2x2') 42const TEXT_EDITABLE_DIALOG = '18.3fp' 43const IMAGE_SIZE = '64vp' 44const MAX_DIALOG = '256vp' 45const MIN_DIALOG = '216vp' 46const RESOURCE_TYPE_SYMBOL: number = 40000; 47 48class ButtonGestureModifier implements GestureModifier { 49 public static readonly longPressTime: number = 500; 50 public static readonly minFontSize: number = 1.75; 51 public fontSize: number = 1; 52 public controller: CustomDialogController | null = null; 53 54 constructor(controller: CustomDialogController | null) { 55 this.controller = controller; 56 } 57 58 applyGesture(event: UIGestureEvent): void { 59 if (this.fontSize >= ButtonGestureModifier.minFontSize) { 60 event.addGesture( 61 new LongPressGestureHandler({ repeat: false, duration: ButtonGestureModifier.longPressTime }) 62 .onAction(() => { 63 if (event) { 64 this.controller?.open(); 65 } 66 }) 67 .onActionEnd(() => { 68 this.controller?.close(); 69 }) 70 ) 71 } else { 72 event.clearGestures(); 73 } 74 } 75} 76 77@Component 78export struct TabTitleBar { 79 tabItems: Array<TabTitleBarTabItem> = []; 80 menuItems: Array<TabTitleBarMenuItem> = []; 81 @BuilderParam swiperContent: () => void 82 83 @State tabWidth: number = 0 84 @State currentIndex: number = 0 85 @State fontSize: number = 1 86 87 static readonly totalHeight = 56 88 static readonly correctionOffset = -40.0 89 static readonly gradientMaskWidth = 24 90 private static instanceCount = 0 91 92 private menuSectionWidth = 0 93 private tabOffsets: Array<number> = []; 94 private imageWidths: Array<number> = []; 95 96 private scroller: Scroller = new Scroller() 97 private swiperController: SwiperController = new SwiperController() 98 private settings: RenderingContextSettings = new RenderingContextSettings(true) 99 private leftContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 100 private rightContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 101 102 @Builder 103 GradientMask(context2D: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number) { 104 Column() { 105 Canvas(context2D) 106 .width(TabTitleBar.gradientMaskWidth) 107 .height(TabTitleBar.totalHeight) 108 .onReady(() => { 109 let grad = context2D.createLinearGradient(x0, y0, x1, y1); 110 grad.addColorStop(0.0, '#ffffffff') 111 grad.addColorStop(1, '#00ffffff') 112 context2D.fillStyle = grad 113 context2D.fillRect(0, 0, TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight) 114 }) 115 } 116 .blendMode(BlendMode.DST_OUT) 117 .width(TabTitleBar.gradientMaskWidth) 118 .height(TabTitleBar.totalHeight) 119 } 120 121 @Builder 122 emptyBuilder() { 123 } 124 125 aboutToAppear() { 126 if (!this.swiperContent) { 127 this.swiperContent = this.emptyBuilder; 128 } 129 this.tabItems.forEach((_elem) => { 130 this.imageWidths.push(0) 131 }) 132 this.loadOffsets() 133 } 134 135 loadOffsets() { 136 this.tabOffsets.length = 0 137 138 let tabOffset = 0 139 this.tabOffsets.push(tabOffset) 140 tabOffset += TabContentItem.marginFirst 141 142 this.tabItems.forEach((tabItem, index) => { 143 if (tabItem.icon !== undefined || tabItem.symbolStyle !== undefined) { 144 if (Math.abs(this.imageWidths[index]) > TabContentItem.imageHotZoneWidth) { 145 tabOffset += this.imageWidths[index] 146 } else { 147 tabOffset += TabContentItem.imageHotZoneWidth 148 } 149 } else { 150 tabOffset += TabContentItem.paddingLeft 151 tabOffset += px2vp(MeasureText.measureText({ 152 textContent: tabItem.title.toString(), 153 fontSize: 18, 154 fontWeight: FontWeight.Medium, 155 })) 156 tabOffset += TabContentItem.paddingRight 157 } 158 this.tabOffsets.push(tabOffset) 159 }) 160 } 161 162 build() { 163 Column() { 164 Flex({ 165 justifyContent: FlexAlign.SpaceBetween, 166 alignItems: ItemAlign.Stretch 167 }) { 168 Stack({ alignContent: Alignment.End }) { 169 Stack({ alignContent: Alignment.Start }) { 170 Column() { 171 List({ initialIndex: 0, scroller: this.scroller, space: 0 }) { 172 ForEach(this.tabItems, (tabItem: TabTitleBarTabItem, index: number) => { 173 ListItem() { 174 TabContentItem({ 175 item: tabItem, 176 index: index, 177 maxIndex: this.tabItems.length - 1, 178 currentIndex: this.currentIndex, 179 onCustomClick: (itemIndex) => this.currentIndex = itemIndex, 180 onImageComplete: (width) => { 181 this.imageWidths[index] = width 182 this.loadOffsets() 183 } 184 }) 185 } 186 }) 187 } 188 .width('100%') 189 .height(TabTitleBar.totalHeight) 190 .constraintSize({ maxWidth: this.tabWidth }) 191 .edgeEffect(EdgeEffect.Spring) 192 .listDirection(Axis.Horizontal) 193 .scrollBar(BarState.Off) 194 } 195 this.GradientMask(this.leftContext2D, 0, TabTitleBar.totalHeight / 2, 196 TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight / 2) 197 } 198 this.GradientMask(this.rightContext2D, TabTitleBar.gradientMaskWidth, 199 TabTitleBar.totalHeight / 2, 0, TabTitleBar.totalHeight / 2) 200 } 201 .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN) 202 203 if (this.menuItems !== undefined && this.menuItems.length > 0) { 204 CollapsibleMenuSection({ menuItems: this.menuItems, index: 1 + TabTitleBar.instanceCount++ }) 205 .height(TabTitleBar.totalHeight) 206 .onAreaChange((_oldValue, newValue) => { 207 this.menuSectionWidth = Number(newValue.width) 208 }) 209 } 210 } 211 .backgroundColor($r('sys.color.ohos_id_color_background')) 212 .margin({ right: $r('sys.float.ohos_id_max_padding_end') }) 213 .onAreaChange((_oldValue, newValue) => { 214 this.tabWidth = Number(newValue.width) - this.menuSectionWidth 215 }) 216 217 Column() { 218 Swiper(this.swiperController) { this.swiperContent() } 219 .index(this.currentIndex) 220 .itemSpace(0) 221 .indicator(false) 222 .width('100%') 223 .height('100%') 224 .curve(Curve.Friction) 225 .onChange((index) => { 226 const offset = this.tabOffsets[index] + TabTitleBar.correctionOffset 227 this.currentIndex = index 228 this.scroller.scrollTo({ 229 xOffset: offset > 0 ? offset : 0, 230 yOffset: 0, 231 animation: { 232 duration: 300, 233 curve: Curve.EaseInOut 234 } 235 }) 236 }) 237 .onAppear(() => { 238 this.scroller.scrollToIndex(this.currentIndex) 239 this.scroller.scrollBy(TabTitleBar.correctionOffset, 0) 240 }) 241 } 242 } 243 } 244} 245 246@Component 247struct CollapsibleMenuSection { 248 menuItems: Array<TabTitleBarMenuItem> = []; 249 index: number = 0; 250 item: TabTitleBarMenuItem = { 251 value: PUBLIC_MORE, 252 symbolStyle: new SymbolGlyphModifier(PUBLIC_MORE), 253 label: $r('sys.string.ohos_toolbar_more'), 254 } as TabTitleBarMenuItem; 255 minFontSize: number = 1.75; 256 isFollowingSystemFontScale: boolean = false; 257 maxFontScale: number = 1; 258 systemFontScale?: number = 1; 259 260 static readonly maxCountOfVisibleItems = 1 261 private static readonly focusPadding = 4 262 private static readonly marginsNum = 2 263 private firstFocusableIndex = -1 264 265 @State isPopupShown: boolean = false 266 267 @State isMoreIconOnFocus: boolean = false 268 @State isMoreIconOnHover: boolean = false 269 @State isMoreIconOnClick: boolean = false 270 @Prop @Watch('onFontSizeUpdated') fontSize: number = 1; 271 272 dialogController: CustomDialogController | null = new CustomDialogController({ 273 builder: TabTitleBarDialog({ 274 cancel: () => { 275 }, 276 confirm: () => { 277 }, 278 tabTitleDialog: this.item, 279 tabTitleBarDialog: this.item.label ? this.item.label : '', 280 fontSize: this.fontSize, 281 }), 282 maskColor: Color.Transparent, 283 isModal: true, 284 customStyle: true, 285 }) 286 287 @State buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(this.dialogController); 288 289 getMoreIconFgColor() { 290 return this.isMoreIconOnClick 291 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 292 : $r('sys.color.ohos_id_color_titlebar_icon') 293 } 294 295 getMoreIconBgColor() { 296 if (this.isMoreIconOnClick) { 297 return $r('sys.color.ohos_id_color_click_effect') 298 } else if (this.isMoreIconOnHover) { 299 return $r('sys.color.ohos_id_color_hover') 300 } else { 301 return Color.Transparent 302 } 303 } 304 305 aboutToAppear() { 306 try { 307 let uiContent: UIContext = this.getUIContext(); 308 this.isFollowingSystemFontScale = uiContent.isFollowingSystemFontScale(); 309 this.maxFontScale = uiContent.getMaxFontScale(); 310 } catch (exception) { 311 let code: number = (exception as BusinessError).code; 312 let message: string = (exception as BusinessError).message; 313 hilog.error(0x3900, 'Ace', `Faild to decideFontScale,cause, code: ${code}, message: ${message}`); 314 } 315 this.menuItems.forEach((item, index) => { 316 if (item.isEnabled && this.firstFocusableIndex === -1 && 317 index > CollapsibleMenuSection.maxCountOfVisibleItems - 2) { 318 this.firstFocusableIndex = this.index * 1000 + index + 1 319 } 320 }) 321 this.fontSize = this.decideFontScale() 322 } 323 324 decideFontScale(): number { 325 let uiContent: UIContext = this.getUIContext(); 326 this.systemFontScale = (uiContent.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1; 327 if (!this.isFollowingSystemFontScale) { 328 return 1; 329 } 330 return Math.min(this.systemFontScale, this.maxFontScale); 331 } 332 333 onFontSizeUpdated(): void { 334 this.buttonGestureModifier.fontSize = this.fontSize; 335 } 336 337 build() { 338 Column() { 339 Row() { 340 if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) { 341 ForEach(this.menuItems, (item: TabTitleBarMenuItem, index: number) => { 342 ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 }) 343 }) 344 } else { 345 ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1), 346 (item: TabTitleBarMenuItem, index: number) => { 347 ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 }) 348 }) 349 350 Button({ type: ButtonType.Normal, stateEffect: true }) { 351 SymbolGlyph(PUBLIC_MORE) 352 .fontSize(TabContentItem.symbolSize) 353 .draggable(false) 354 .fontColor([$r('sys.color.icon_primary')]) 355 .focusable(true) 356 } 357 .accessibilityText($r('sys.string.ohos_toolbar_more')) 358 .width(ImageMenuItem.imageHotZoneWidth) 359 .height(ImageMenuItem.imageHotZoneWidth) 360 .borderRadius(ImageMenuItem.buttonBorderRadius) 361 .foregroundColor(this.getMoreIconFgColor()) 362 .backgroundColor(this.getMoreIconBgColor()) 363 .stateStyles({ 364 focused: { 365 .border({ 366 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 367 width: ImageMenuItem.focusBorderWidth, 368 color: $r('sys.color.ohos_id_color_focused_outline'), 369 style: BorderStyle.Solid 370 }) 371 }, 372 normal: { 373 .border({ 374 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 375 width: 0 376 }) 377 } 378 }) 379 .onFocus(() => this.isMoreIconOnFocus = true) 380 .onBlur(() => this.isMoreIconOnFocus = false) 381 .onHover((isOn) => this.isMoreIconOnHover = isOn) 382 .onKeyEvent((event) => { 383 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 384 return 385 } 386 if (event.type === KeyType.Down) { 387 this.isMoreIconOnClick = true 388 } 389 if (event.type === KeyType.Up) { 390 this.isMoreIconOnClick = false 391 } 392 }) 393 .onTouch((event) => { 394 if (event.type === TouchType.Down) { 395 this.isMoreIconOnClick = true 396 } 397 if (event.type === TouchType.Up || event.type === TouchType.Cancel) { 398 this.isMoreIconOnClick = false 399 if (this.fontSize >= this.minFontSize) { 400 this.dialogController?.close() 401 } 402 } 403 }) 404 .onClick(() => this.isPopupShown = true) 405 .gestureModifier(this.buttonGestureModifier) 406 .bindPopup(this.isPopupShown, { 407 builder: this.popupBuilder, 408 placement: Placement.Bottom, 409 popupColor: Color.White, 410 enableArrow: false, 411 onStateChange: (e) => { 412 this.isPopupShown = e.isVisible 413 if (!e.isVisible) { 414 this.isMoreIconOnClick = false 415 } 416 } 417 }) 418 } 419 } 420 } 421 .height('100%') 422 .justifyContent(FlexAlign.Center) 423 } 424 425 onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void { 426 children.forEach((child) => { 427 child.layout({ x: 0, y: 0 }); 428 }) 429 this.fontSize = this.decideFontScale(); 430 } 431 432 @Builder 433 popupBuilder() { 434 Column() { 435 ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length), 436 (item: TabTitleBarMenuItem, index: number) => { 437 ImageMenuItem({ item: item, index: this.index * 1000 + 438 CollapsibleMenuSection.maxCountOfVisibleItems + index }) 439 }) 440 } 441 .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum) 442 .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding }) 443 .onAppear(() => { 444 focusControl.requestFocus(ImageMenuItem.focusablePrefix + this.firstFocusableIndex) 445 }) 446 } 447} 448 449@Component 450struct TabContentItem { 451 item: TabTitleBarTabItem = { title: '' }; 452 index: number = 0; 453 maxIndex: number = 0; 454 onCustomClick?: (index: number) => void 455 onImageComplete?: (width: number) => void 456 457 @Prop currentIndex: number 458 459 @State isOnFocus: boolean = false 460 @State isOnHover: boolean = false 461 @State isOnClick: boolean = false 462 @State tabWidth: number = 0 463 464 @State imageWidth: number = 24 465 @State imageHeight: number = 24 466 467 static readonly imageSize = 24 468 static readonly symbolSize = '24vp' 469 static readonly imageHotZoneWidth = 48 470 static readonly imageMagnificationFactor = 1.4 471 static readonly buttonBorderRadius = 8 472 static readonly focusBorderWidth = 2 473 static readonly paddingLeft = 8 474 static readonly paddingRight = 8 475 static readonly marginFirst = 16 476 477 getBgColor() { 478 if (this.isOnClick) { 479 return $r('sys.color.ohos_id_color_click_effect') 480 } else if (this.isOnHover) { 481 return $r('sys.color.ohos_id_color_hover') 482 } else { 483 return Color.Transparent 484 } 485 } 486 487 getBorderAttr(): BorderOptions { 488 if (this.isOnFocus) { 489 return { 490 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 491 width: TabContentItem.focusBorderWidth, 492 color: $r('sys.color.ohos_id_color_focused_outline'), 493 style: BorderStyle.Solid 494 } 495 } 496 return { width: 0 } 497 } 498 499 getImageScaleFactor(): number { 500 return this.index === this.currentIndex ? TabContentItem.imageMagnificationFactor : 1 501 } 502 503 getImageLayoutWidth(): number { 504 return TabContentItem.imageSize / Math.max(this.imageHeight, 1.0) * this.imageWidth 505 } 506 507 private toStringFormat(resource: ResourceStr | undefined): string | undefined { 508 if (typeof resource === 'string') { 509 return resource; 510 } else if (typeof resource === 'undefined') { 511 return ''; 512 } else { 513 let resourceString: string = ''; 514 try { 515 resourceString = getContext()?.resourceManager?.getStringSync(resource); 516 } catch (err) { 517 let code: number = (err as BusinessError)?.code; 518 let message: string = (err as BusinessError)?.message; 519 hilog.error(0x3900, 'Ace', `Faild to TabTitleBar toStringFormat,code: ${code},message:${message}`); 520 } 521 return resourceString; 522 } 523 } 524 525 build() { 526 Stack() { 527 Row() { 528 Column() { 529 if (this.item.icon === undefined && this.item.symbolStyle === undefined) { 530 Text(this.item.title) 531 .fontSize(this.index === this.currentIndex 532 ? $r('sys.float.ohos_id_text_size_headline7') 533 : $r('sys.float.ohos_id_text_size_headline9')) 534 .fontColor(this.index === this.currentIndex 535 ? $r('sys.color.ohos_id_color_titlebar_text') 536 : $r('sys.color.ohos_id_color_titlebar_text_off')) 537 .fontWeight(FontWeight.Medium) 538 .focusable(true) 539 .animation({ duration: 300 }) 540 .padding({ 541 top: this.index === this.currentIndex ? 6 : 10, 542 left: TabContentItem.paddingLeft, 543 bottom: 2, 544 right: TabContentItem.paddingRight 545 }) 546 .onFocus(() => this.isOnFocus = true) 547 .onBlur(() => this.isOnFocus = false) 548 .onHover((isOn) => this.isOnHover = isOn) 549 .onKeyEvent((event) => { 550 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 551 return 552 } 553 if (event.type === KeyType.Down) { 554 this.isOnClick = true 555 } 556 if (event.type === KeyType.Up) { 557 this.isOnClick = false 558 } 559 }) 560 .onTouch((event) => { 561 if (event.type === TouchType.Down) { 562 this.isOnClick = true 563 } 564 if (event.type === TouchType.Up || event.type === TouchType.Cancel) { 565 this.isOnClick = false 566 } 567 }) 568 .onClick(() => this.onCustomClick && this.onCustomClick(this.index)) 569 .accessibilitySelected(this.index === this.currentIndex) 570 } else { 571 Row() { 572 if (this.item.symbolStyle) { 573 SymbolGlyph() 574 .fontColor([$r('sys.color.icon_primary')]) 575 .attributeModifier(this.item.symbolStyle) 576 .fontSize(TabContentItem.symbolSize) 577 .width(this.getImageLayoutWidth()) 578 .height(TabContentItem.imageSize) 579 .accessibilityText(this.toStringFormat(this.item.title)) 580 .scale({ 581 x: this.getImageScaleFactor(), 582 y: this.getImageScaleFactor() 583 }) 584 .animation({ duration: 300 }) 585 .hitTestBehavior(HitTestMode.None) 586 .focusable(true) 587 .symbolEffect(new SymbolEffect(), false) 588 } else { 589 if (Util.isSymbolResource(this.item.icon)) { 590 SymbolGlyph(this.item.icon as Resource) 591 .fontColor([$r('sys.color.icon_primary')]) 592 .fontSize(TabContentItem.symbolSize) 593 .width(this.getImageLayoutWidth()) 594 .height(TabContentItem.imageSize) 595 .accessibilityText(this.toStringFormat(this.item.title)) 596 .scale({ 597 x: this.getImageScaleFactor(), 598 y: this.getImageScaleFactor() 599 }) 600 .animation({ duration: 300 }) 601 .hitTestBehavior(HitTestMode.None) 602 .focusable(true) 603 } else { 604 Image(this.item.icon) 605 .alt(this.item.title) 606 .width(this.getImageLayoutWidth()) 607 .height(TabContentItem.imageSize) 608 .objectFit(ImageFit.Fill) 609 .accessibilityText(this.toStringFormat(this.item.title)) 610 .scale({ 611 x: this.getImageScaleFactor(), 612 y: this.getImageScaleFactor() 613 }) 614 .animation({ duration: 300 }) 615 .hitTestBehavior(HitTestMode.None) 616 .focusable(true) 617 .onComplete((event) => { 618 if (!this.onImageComplete) { 619 return 620 } 621 this.imageWidth = px2vp(event?.width); 622 this.imageHeight = px2vp(event?.height); 623 this.onImageComplete(px2vp(event?.componentWidth) + 624 TabContentItem.paddingLeft + TabContentItem.paddingRight); 625 }) 626 .onError((event) => { 627 if (!this.onImageComplete) { 628 return 629 } 630 this.onImageComplete(px2vp(event.componentWidth) + 631 TabContentItem.paddingLeft + TabContentItem.paddingRight) 632 }) 633 } 634 } 635 } 636 .width(this.getImageLayoutWidth() * this.getImageScaleFactor() + 637 TabContentItem.paddingLeft + TabContentItem.paddingRight) 638 .constraintSize({ 639 minWidth: TabContentItem.imageHotZoneWidth, 640 minHeight: TabContentItem.imageHotZoneWidth 641 }) 642 .animation({ duration: 300 }) 643 .justifyContent(FlexAlign.Center) 644 .onFocus(() => this.isOnFocus = true) 645 .onBlur(() => this.isOnFocus = false) 646 .onHover((isOn) => this.isOnHover = isOn) 647 .onKeyEvent((event) => { 648 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 649 return 650 } 651 if (event.type === KeyType.Down) { 652 this.isOnClick = true 653 } 654 if (event.type === KeyType.Up) { 655 this.isOnClick = false 656 } 657 }) 658 .onTouch((event) => { 659 if (event.type === TouchType.Down) { 660 this.isOnClick = true 661 } 662 if (event.type === TouchType.Up || event.type === TouchType.Cancel) { 663 this.isOnClick = false 664 } 665 }) 666 .onClick(() => this.onCustomClick && this.onCustomClick(this.index)) 667 .accessibilitySelected(this.index === this.currentIndex) 668 } 669 } 670 .justifyContent(FlexAlign.Center) 671 } 672 .height(TabTitleBar.totalHeight) 673 .alignItems(VerticalAlign.Center) 674 .justifyContent(FlexAlign.Center) 675 .borderRadius(TabContentItem.buttonBorderRadius) 676 .backgroundColor(this.getBgColor()) 677 .onAreaChange((_oldValue, newValue) => { 678 this.tabWidth = Number(newValue.width) 679 }) 680 681 if (this.isOnFocus && this.tabWidth > 0) { 682 Row() 683 .width(this.tabWidth) 684 .height(TabTitleBar.totalHeight) 685 .hitTestBehavior(HitTestMode.None) 686 .borderRadius(TabContentItem.buttonBorderRadius) 687 .stateStyles({ 688 focused: { 689 .border(this.getBorderAttr()) 690 }, 691 normal: { 692 .border({ 693 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 694 width: 0 695 }) 696 } 697 }) 698 } 699 } 700 .margin({ 701 left: this.index === 0 ? TabContentItem.marginFirst : 0, 702 right: this.index === this.maxIndex ? 12 : 0 703 }) 704 } 705} 706 707@Component 708struct ImageMenuItem { 709 item: TabTitleBarMenuItem = { value: '' }; 710 index: number = 0; 711 712 static readonly imageSize = 24 713 static readonly imageHotZoneWidth = 48 714 static readonly buttonBorderRadius = 8 715 static readonly focusBorderWidth = 2 716 static readonly disabledImageOpacity = 0.4 717 static readonly focusablePrefix = "Id-TabTitleBar-ImageMenuItem-" 718 719 @State isOnFocus: boolean = false 720 @State isOnHover: boolean = false 721 @State isOnClick: boolean = false 722 723 getFgColor() { 724 return this.isOnClick 725 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 726 : $r('sys.color.ohos_id_color_titlebar_icon') 727 } 728 729 getBgColor() { 730 if (this.isOnClick) { 731 return $r('sys.color.ohos_id_color_click_effect') 732 } else if (this.isOnHover) { 733 return $r('sys.color.ohos_id_color_hover') 734 } else { 735 return Color.Transparent 736 } 737 } 738 739 private toStringFormat(resource: ResourceStr | undefined): string | undefined { 740 if (typeof resource === 'string') { 741 return resource; 742 } else if (typeof resource === 'undefined') { 743 return ''; 744 } else { 745 let resourceString: string = ''; 746 try { 747 resourceString = getContext()?.resourceManager?.getStringSync(resource); 748 } catch (err) { 749 let code: number = (err as BusinessError)?.code; 750 let message: string = (err as BusinessError)?.message; 751 hilog.error(0x3900, 'Ace', `Faild to TabTitleBar toStringFormat,code: ${code},message:${message}`); 752 } 753 return resourceString; 754 } 755 } 756 757 private getAccessibilityReadText(): string | undefined { 758 if (this.item.value === PUBLIC_MORE) { 759 return getContext()?.resourceManager?.getStringByNameSync('ohos_toolbar_more'); 760 } else if (this.item.accessibilityText) { 761 return this.toStringFormat(this.item.accessibilityText); 762 } else if (this.item.label) { 763 return this.toStringFormat(this.item.label); 764 } 765 return ' '; 766 } 767 768 build() { 769 Button({ type: ButtonType.Normal, stateEffect: this.item.isEnabled }) { 770 if (this.item.symbolStyle) { 771 SymbolGlyph() 772 .fontColor([$r('sys.color.font_primary')]) 773 .attributeModifier(this.item.symbolStyle) 774 .fontSize(TabContentItem.symbolSize) 775 .draggable(false) 776 .focusable(this.item?.isEnabled) 777 .key(ImageMenuItem.focusablePrefix + this.index) 778 .symbolEffect(new SymbolEffect(), false) 779 } else { 780 if (Util.isSymbolResource(this.item.value)) { 781 SymbolGlyph(this.item.value as Resource) 782 .fontColor([$r('sys.color.font_primary')]) 783 .fontSize(TabContentItem.symbolSize) 784 .draggable(false) 785 .focusable(this.item?.isEnabled) 786 .key(ImageMenuItem.focusablePrefix + this.index) 787 } else { 788 Image(this.item.value) 789 .width(ImageMenuItem.imageSize) 790 .height(ImageMenuItem.imageSize) 791 .focusable(this.item.isEnabled) 792 .key(ImageMenuItem.focusablePrefix + this.index) 793 .draggable(false) 794 } 795 } 796 } 797 .accessibilityText(this.getAccessibilityReadText()) 798 .accessibilityLevel(this.item?.accessibilityLevel ?? 'auto') 799 .accessibilityDescription(this.toStringFormat(this.item?.accessibilityDescription)) 800 .width(ImageMenuItem.imageHotZoneWidth) 801 .height(ImageMenuItem.imageHotZoneWidth) 802 .borderRadius(ImageMenuItem.buttonBorderRadius) 803 .foregroundColor(this.getFgColor()) 804 .backgroundColor(this.getBgColor()) 805 .enabled(this.item.isEnabled ? this.item.isEnabled : false) 806 .stateStyles({ 807 focused: { 808 .border({ 809 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 810 width: ImageMenuItem.focusBorderWidth, 811 color: $r('sys.color.ohos_id_color_focused_outline'), 812 style: BorderStyle.Solid 813 }) 814 }, 815 normal: { 816 .border({ 817 radius: $r('sys.float.ohos_id_corner_radius_clicked'), 818 width: 0 819 }) 820 } 821 }) 822 .onFocus(() => { 823 if (!this.item.isEnabled) { 824 return 825 } 826 this.isOnFocus = true 827 }) 828 .onBlur(() => this.isOnFocus = false) 829 .onHover((isOn) => { 830 if (!this.item.isEnabled) { 831 return 832 } 833 this.isOnHover = isOn 834 }) 835 .onKeyEvent((event) => { 836 if (!this.item.isEnabled) { 837 return 838 } 839 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 840 return 841 } 842 if (event.type === KeyType.Down) { 843 this.isOnClick = true 844 } 845 if (event.type === KeyType.Up) { 846 this.isOnClick = false 847 } 848 }) 849 .onTouch((event) => { 850 if (!this.item.isEnabled) { 851 return 852 } 853 if (event.type === TouchType.Down) { 854 this.isOnClick = true 855 } 856 if (event.type === TouchType.Up || event.type === TouchType.Cancel) { 857 this.isOnClick = false 858 } 859 }) 860 .onClick(() => this.item.isEnabled && this.item.action && this.item.action()) 861 } 862} 863 864/** 865 * TabTitleBarDialog 866 */ 867@CustomDialog 868struct TabTitleBarDialog { 869 tabTitleDialog: TabTitleBarMenuItem = { value: '' }; 870 callbackId: number | undefined = undefined; 871 tabTitleBarDialog?: ResourceStr = ''; 872 mainWindowStage: window.Window | undefined = undefined; 873 controller?: CustomDialogController 874 minFontSize: number = 1.75; 875 maxFontSize: number = 3.2; 876 screenWidth: number = 640; 877 verticalScreenLines: number = 6; 878 horizontalsScreenLines: number = 1; 879 @StorageLink('mainWindow') mainWindow: Promise<window.Window> | undefined = undefined; 880 @State fontSize: number = 1; 881 @State maxLines: number = 1; 882 @StorageProp('windowStandardHeight') windowStandardHeight: number = 0; 883 cancel: () => void = () => { 884 } 885 confirm: () => void = () => { 886 } 887 888 build() { 889 if (this.tabTitleBarDialog) { 890 Column() { 891 if (this.tabTitleDialog.symbolStyle) { 892 SymbolGlyph() 893 .fontColor([$r('sys.color.font_primary')]) 894 .attributeModifier(this.tabTitleDialog.symbolStyle) 895 .fontSize(IMAGE_SIZE) 896 .draggable(false) 897 .focusable(this.tabTitleDialog?.isEnabled) 898 .margin({ 899 top: $r('sys.float.padding_level24'), 900 bottom: $r('sys.float.padding_level8'), 901 }) 902 .symbolEffect(new SymbolEffect(), false) 903 } else if (this.tabTitleDialog.value) { 904 if (Util.isSymbolResource(this.tabTitleDialog.value)) { 905 SymbolGlyph(this.tabTitleDialog.value as Resource) 906 .fontColor([$r('sys.color.font_primary')]) 907 .fontSize(IMAGE_SIZE) 908 .draggable(false) 909 .focusable(this.tabTitleDialog?.isEnabled) 910 .margin({ 911 top: $r('sys.float.padding_level24'), 912 bottom: $r('sys.float.padding_level8'), 913 }) 914 } else { 915 Image(this.tabTitleDialog.value) 916 .width(IMAGE_SIZE) 917 .height(IMAGE_SIZE) 918 .margin({ 919 top: $r('sys.float.padding_level24'), 920 bottom: $r('sys.float.padding_level8'), 921 }) 922 .fillColor($r('sys.color.icon_primary')) 923 } 924 } 925 Column() { 926 Text(this.tabTitleBarDialog) 927 .fontSize(TEXT_EDITABLE_DIALOG) 928 .textOverflow({ overflow: TextOverflow.Ellipsis }) 929 .maxLines(this.maxLines) 930 .width('100%') 931 .textAlign(TextAlign.Center) 932 .fontColor($r('sys.color.font_primary')) 933 } 934 .width('100%') 935 .padding({ 936 left: $r('sys.float.padding_level4'), 937 right: $r('sys.float.padding_level4'), 938 bottom: $r('sys.float.padding_level12'), 939 }) 940 } 941 .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG) 942 .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG }) 943 .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) 944 .shadow(ShadowStyle.OUTER_DEFAULT_LG) 945 .borderRadius($r('sys.float.corner_radius_level10')) 946 } else { 947 Column() { 948 if (this.tabTitleDialog.symbolStyle) { 949 SymbolGlyph() 950 .fontColor([$r('sys.color.font_primary')]) 951 .attributeModifier(this.tabTitleDialog.symbolStyle) 952 .fontSize(IMAGE_SIZE) 953 .draggable(false) 954 .focusable(this.tabTitleDialog?.isEnabled) 955 .symbolEffect(new SymbolEffect(), false) 956 } else if (this.tabTitleDialog.value){ 957 if (Util.isSymbolResource(this.tabTitleDialog.value)) { 958 SymbolGlyph(this.tabTitleDialog.value as Resource) 959 .fontColor([$r('sys.color.font_primary')]) 960 .fontSize(IMAGE_SIZE) 961 .draggable(false) 962 .focusable(this.tabTitleDialog?.isEnabled) 963 .margin({ 964 top: $r('sys.float.padding_level24'), 965 bottom: $r('sys.float.padding_level8'), 966 }) 967 } else { 968 Image(this.tabTitleDialog.value) 969 .width(IMAGE_SIZE) 970 .height(IMAGE_SIZE) 971 .fillColor($r('sys.color.icon_primary')) 972 } 973 } 974 } 975 .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG) 976 .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG }) 977 .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) 978 .shadow(ShadowStyle.OUTER_DEFAULT_LG) 979 .borderRadius($r('sys.float.corner_radius_level10')) 980 .justifyContent(FlexAlign.Center) 981 } 982 } 983 984 async aboutToAppear(): Promise<void> { 985 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 986 this.mainWindowStage = context.windowStage.getMainWindowSync(); 987 let properties: window.WindowProperties = this.mainWindowStage.getWindowProperties(); 988 let rect = properties.windowRect; 989 if (px2vp(rect.height) > this.screenWidth) { 990 this.maxLines = this.verticalScreenLines; 991 } else { 992 this.maxLines = this.horizontalsScreenLines; 993 } 994 } 995} 996 997class Util { 998 public static isSymbolResource(resourceStr: ResourceStr | undefined): boolean { 999 if (!Util.isResourceType(resourceStr)) { 1000 return false; 1001 } 1002 let resource = resourceStr as Resource; 1003 return resource.type === RESOURCE_TYPE_SYMBOL; 1004 } 1005 1006 public static isResourceType(resource: ResourceStr | Resource | undefined): boolean { 1007 if (!resource) { 1008 return false; 1009 } 1010 if (typeof resource === 'string' || typeof resource === 'undefined') { 1011 return false; 1012 } 1013 return true; 1014 } 1015}