1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { 17 ColorMetrics, 18 curves, 19 ImageModifier, 20 LengthMetrics, 21 LengthUnit, 22 SymbolGlyphModifier, 23 TextModifier, 24 UIContext, 25} from '@kit.ArkUI'; 26import { SizeT } from '@ohos.arkui.node'; 27import { i18n } from '@kit.LocalizationKit'; 28import util from '@ohos.util'; 29 30export interface SegmentButtonV2ItemOptions { 31 text?: ResourceStr; 32 icon?: ResourceStr; 33 symbol?: Resource; 34 enabled?: boolean; 35 textModifier?: TextModifier; 36 iconModifier?: ImageModifier; 37 symbolModifier?: SymbolGlyphModifier; 38 accessibilityText?: ResourceStr; 39 accessibilityDescription?: ResourceStr; 40 accessibilityLevel?: string; 41} 42 43export type OnSelectedIndexChange = (selectedIndex: number) => void; 44 45export type OnSelectedIndexesChange = (selectedIndexes: number[]) => void; 46 47interface SegmentButtonV2ContentTheme { 48 itemSpace: LengthMetrics; 49 itemFontSize: Dimension; 50 itemFontColor: ResourceColor; 51 itemFontWeight: FontWeight; 52 itemSelectedFontWeight: FontWeight; 53 itemSelectedFontColor: ResourceColor; 54 itemIconSize: Dimension; 55 itemIconFillColor: ResourceColor; 56 itemSelectedIconFillColor: ResourceColor; 57 itemSymbolFontSize: Dimension; 58 itemSymbolFontColor: ResourceColor; 59 itemSelectedSymbolFontColor: ResourceColor; 60 itemMinHeight: Dimension; 61 hybridItemMinHeight: Dimension; 62 itemPadding: LocalizedPadding; 63 itemMaxFontScale: number | Resource; 64 itemMaxFontScaleSmallest: number; 65 itemMaxFontScaleLargest: number; 66 itemMinFontScale: number | Resource; 67 itemMinFontScaleSmallest: number; 68 itemMinFontScaleLargest: number; 69} 70 71interface SimpleSegmentButtonV2Theme extends SegmentButtonV2ContentTheme { 72 buttonBackgroundColor: Resource; 73 buttonBorderRadius: Resource; 74 buttonMinHeight: Dimension; 75 hybridButtonMinHeight: Dimension; 76 buttonPadding: Resource; 77 itemSelectedBackgroundColor: ResourceColor; 78 itemBorderRadius: Resource; 79 itemShadow: ShadowStyle; 80} 81 82interface SegmentButtonV2ItemRect { 83 size: SizeT<number>; 84 position: PositionT<number>; 85 globalPosition: PositionT<number>; 86} 87 88const SMALLEST_MAX_FONT_SCALE: number = 1; 89const LARGEST_MAX_FONT_SCALE: number = 2; 90const SMALLEST_MIN_FONT_SCALE: number = 0; 91const LARGEST_MIN_FONT_SCALE: number = 1; 92 93const tabSimpleTheme: SimpleSegmentButtonV2Theme = { 94 buttonBackgroundColor: $r('sys.color.segment_button_v2_tab_button_background'), 95 buttonBorderRadius: $r('sys.float.segment_button_v2_background_corner_radius'), 96 buttonMinHeight: $r('sys.float.segment_button_v2_singleline_background_height'), 97 hybridButtonMinHeight: $r('sys.float.segment_button_v2_doubleline_background_height'), 98 buttonPadding: $r('sys.float.padding_level1'), 99 itemSelectedBackgroundColor: $r('sys.color.segment_button_v2_tab_selected_item_background'), 100 itemBorderRadius: $r('sys.float.segment_button_v2_selected_corner_radius'), 101 itemSpace: LengthMetrics.vp(0), 102 itemFontSize: $r('sys.float.ohos_id_text_size_button2'), 103 itemFontColor: $r('sys.color.font_secondary'), 104 itemSelectedFontColor: $r('sys.color.font_primary'), 105 itemFontWeight: FontWeight.Medium, 106 itemSelectedFontWeight: FontWeight.Medium, 107 itemIconSize: 24, 108 itemIconFillColor: $r('sys.color.font_secondary'), 109 itemSelectedIconFillColor: $r('sys.color.font_primary'), 110 itemSymbolFontSize: 20, 111 itemSymbolFontColor: $r('sys.color.font_secondary'), 112 itemSelectedSymbolFontColor: $r('sys.color.font_primary'), 113 itemMinHeight: $r('sys.float.segment_button_v2_singleline_selected_height'), 114 hybridItemMinHeight: $r('sys.float.segment_button_v2_doubleline_selected_height'), 115 itemPadding: { 116 top: LengthMetrics.resource($r('sys.float.padding_level2')), 117 bottom: LengthMetrics.resource($r('sys.float.padding_level2')), 118 start: LengthMetrics.resource($r('sys.float.padding_level4')), 119 end: LengthMetrics.resource($r('sys.float.padding_level4')), 120 }, 121 itemShadow: ShadowStyle.OUTER_DEFAULT_XS, 122 itemMaxFontScale: SMALLEST_MAX_FONT_SCALE, 123 itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE, 124 itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE, 125 itemMinFontScale: SMALLEST_MIN_FONT_SCALE, 126 itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE, 127 itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE, 128}; 129 130const capsuleSimpleTheme: SimpleSegmentButtonV2Theme = { 131 buttonBackgroundColor: $r('sys.color.segment_button_v2_tab_button_background'), 132 buttonBorderRadius: $r('sys.float.segment_button_v2_background_corner_radius'), 133 buttonMinHeight: $r('sys.float.segment_button_v2_singleline_background_height'), 134 hybridButtonMinHeight: $r('sys.float.segment_button_v2_doubleline_background_height'), 135 buttonPadding: $r('sys.float.padding_level1'), 136 itemSelectedBackgroundColor: $r('sys.color.comp_background_emphasize'), 137 itemBorderRadius: $r('sys.float.segment_button_v2_selected_corner_radius'), 138 itemSpace: LengthMetrics.vp(0), 139 itemFontSize: $r('sys.float.ohos_id_text_size_button2'), 140 itemFontColor: $r('sys.color.font_secondary'), 141 itemSelectedFontColor: $r('sys.color.font_on_primary'), 142 itemFontWeight: FontWeight.Medium, 143 itemSelectedFontWeight: FontWeight.Medium, 144 itemIconSize: 24, 145 itemIconFillColor: $r('sys.color.icon_secondary'), 146 itemSelectedIconFillColor: $r('sys.color.font_on_primary'), 147 itemSymbolFontSize: 20, 148 itemSymbolFontColor: $r('sys.color.font_secondary'), 149 itemSelectedSymbolFontColor: $r('sys.color.font_on_primary'), 150 itemMinHeight: $r('sys.float.segment_button_v2_singleline_selected_height'), 151 hybridItemMinHeight: $r('sys.float.segment_button_v2_doubleline_selected_height'), 152 itemPadding: { 153 top: LengthMetrics.resource($r('sys.float.padding_level2')), 154 bottom: LengthMetrics.resource($r('sys.float.padding_level2')), 155 start: LengthMetrics.resource($r('sys.float.padding_level4')), 156 end: LengthMetrics.resource($r('sys.float.padding_level4')), 157 }, 158 itemShadow: ShadowStyle.OUTER_DEFAULT_XS, 159 itemMaxFontScale: SMALLEST_MAX_FONT_SCALE, 160 itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE, 161 itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE, 162 itemMinFontScale: SMALLEST_MIN_FONT_SCALE, 163 itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE, 164 itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE, 165} 166 167@ObservedV2 168export class SegmentButtonV2Item { 169 @Trace text?: ResourceStr; 170 @Trace icon?: ResourceStr; 171 @Trace symbol?: Resource; 172 @Trace enabled: boolean; 173 @Trace textModifier?: TextModifier; 174 @Trace iconModifier?: ImageModifier; 175 @Trace symbolModifier?: SymbolGlyphModifier; 176 @Trace accessibilityText?: ResourceStr; 177 @Trace accessibilityDescription?: ResourceStr; 178 @Trace accessibilityLevel?: string; 179 180 constructor(options: SegmentButtonV2ItemOptions) { 181 this.text = options.text; 182 this.icon = options.icon; 183 this.symbol = options.symbol; 184 this.enabled = options.enabled ?? true; 185 this.textModifier = options.textModifier; 186 this.iconModifier = options.iconModifier; 187 this.symbolModifier = options.symbolModifier; 188 this.accessibilityText = options.accessibilityText; 189 this.accessibilityDescription = options.accessibilityDescription; 190 this.accessibilityLevel = options.accessibilityLevel; 191 } 192 193 @Computed 194 get isHybrid(): boolean { 195 return !!this.text && (!!this.icon || !!this.symbol); 196 } 197} 198 199@ObservedV2 200export class SegmentButtonV2Items extends Array<SegmentButtonV2Item> { 201 constructor(length: number); 202 203 constructor(items: SegmentButtonV2ItemOptions[]); 204 205 constructor(lengthOrItemOptionsArray: number | SegmentButtonV2ItemOptions[]) { 206 super(typeof lengthOrItemOptionsArray === 'number' ? lengthOrItemOptionsArray : 0); 207 208 if (typeof lengthOrItemOptionsArray !== 'number' && lengthOrItemOptionsArray && lengthOrItemOptionsArray.length) { 209 for (let options of lengthOrItemOptionsArray) { 210 if (options) { 211 this.push(new SegmentButtonV2Item(options)) 212 } 213 } 214 } 215 } 216 217 @Computed 218 get hasHybrid(): boolean { 219 return this.some((item) => item.isHybrid); 220 } 221} 222 223const EMPTY_ITEMS = new SegmentButtonV2Items([]); 224 225@ComponentV2 226export struct TabSegmentButtonV2 { 227 @Require 228 @Param 229 items: SegmentButtonV2Items; 230 @Require 231 @Param 232 selectedIndex: number; 233 @Event 234 $selectedIndex?: OnSelectedIndexChange; 235 @Event 236 onItemClicked?: Callback<number>; 237 @Param 238 itemMinFontScale?: number | Resource = undefined; 239 @Param 240 itemMaxFontScale?: number | Resource = undefined; 241 @Param 242 itemSpace?: LengthMetrics = undefined; 243 @Param 244 itemFontSize?: LengthMetrics = undefined; 245 @Param 246 itemSelectedFontSize?: LengthMetrics = undefined; 247 @Param 248 itemFontColor?: ColorMetrics = undefined; 249 @Param 250 itemSelectedFontColor?: ColorMetrics = undefined; 251 @Param 252 itemFontWeight?: FontWeight = undefined; 253 @Param 254 itemSelectedFontWeight?: FontWeight = undefined; 255 @Param 256 itemBorderRadius?: LengthMetrics = undefined; 257 @Param 258 itemSelectedBackgroundColor?: ColorMetrics = undefined; 259 @Param 260 itemIconSize?: SizeT<LengthMetrics> = undefined; 261 @Param 262 itemIconFillColor?: ColorMetrics = undefined; 263 @Param 264 itemSelectedIconFillColor?: ColorMetrics = undefined; 265 @Param 266 itemSymbolFontSize?: LengthMetrics = undefined; 267 @Param 268 itemSymbolFontColor?: ColorMetrics = undefined; 269 @Param 270 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 271 @Param 272 itemMinHeight?: LengthMetrics = undefined; 273 @Param 274 itemPadding?: LocalizedPadding = undefined; 275 @Param 276 itemShadow?: ShadowOptions | ShadowStyle = undefined; 277 @Param 278 buttonBackgroundColor?: ColorMetrics = undefined; 279 @Param 280 buttonBackgroundBlurStyle?: BlurStyle = undefined; 281 @Param 282 buttonBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined; 283 @Param 284 buttonBackgroundEffect?: BackgroundEffectOptions = undefined; 285 @Param 286 buttonBorderRadius?: LengthMetrics = undefined; 287 @Param 288 buttonMinHeight?: LengthMetrics = undefined; 289 @Param 290 buttonPadding?: LengthMetrics = undefined; 291 @Param 292 languageDirection?: Direction = undefined; 293 294 build() { 295 SimpleSegmentButtonV2({ 296 theme: tabSimpleTheme, 297 items: this.items, 298 selectedIndex: this.selectedIndex, 299 $selectedIndex: (selectedIndex) => { 300 this.$selectedIndex?.(selectedIndex); 301 }, 302 onItemClicked: this.onItemClicked, 303 itemMinFontScale: this.itemMinFontScale, 304 itemMaxFontScale: this.itemMaxFontScale, 305 itemSpace: this.itemSpace, 306 itemFontColor: this.itemFontColor, 307 itemSelectedFontColor: this.itemSelectedFontColor, 308 itemFontSize: this.itemFontSize, 309 itemSelectedFontSize: this.itemSelectedFontSize, 310 itemFontWeight: this.itemFontWeight, 311 itemSelectedFontWeight: this.itemSelectedFontWeight, 312 itemSelectedBackgroundColor: this.itemSelectedBackgroundColor, 313 itemIconSize: this.itemIconSize, 314 itemIconFillColor: this.itemIconFillColor, 315 itemSelectedIconFillColor: this.itemSelectedIconFillColor, 316 itemSymbolFontSize: this.itemSymbolFontSize, 317 itemSymbolFontColor: this.itemSymbolFontColor, 318 itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor, 319 itemBorderRadius: this.itemBorderRadius, 320 itemMinHeight: this.itemMinHeight, 321 itemPadding: this.itemPadding, 322 itemShadow: this.itemShadow, 323 buttonBackgroundColor: this.buttonBackgroundColor, 324 buttonBackgroundBlurStyle: this.buttonBackgroundBlurStyle, 325 buttonBackgroundBlurStyleOptions: this.buttonBackgroundBlurStyleOptions, 326 buttonBackgroundEffect: this.buttonBackgroundEffect, 327 buttonBorderRadius: this.buttonBorderRadius, 328 buttonMinHeight: this.buttonMinHeight, 329 buttonPadding: this.buttonPadding, 330 languageDirection: this.languageDirection, 331 }) 332 } 333} 334 335@ComponentV2 336export struct CapsuleSegmentButtonV2 { 337 @Require 338 @Param 339 items: SegmentButtonV2Items; 340 @Require 341 @Param 342 selectedIndex: number; 343 @Event 344 $selectedIndex?: OnSelectedIndexChange; 345 @Event 346 onItemClicked?: Callback<number>; 347 @Param 348 itemMinFontScale?: number | Resource = undefined; 349 @Param 350 itemMaxFontScale?: number | Resource = undefined; 351 @Param 352 itemSpace?: LengthMetrics = undefined; 353 @Param 354 itemFontColor?: ColorMetrics = undefined; 355 @Param 356 itemSelectedFontColor?: ColorMetrics = undefined; 357 @Param 358 itemFontSize?: LengthMetrics = undefined; 359 @Param 360 itemSelectedFontSize?: LengthMetrics = undefined; 361 @Param 362 itemFontWeight?: FontWeight = undefined; 363 @Param 364 itemSelectedFontWeight?: FontWeight = undefined; 365 @Param 366 itemBorderRadius?: LengthMetrics = undefined; 367 @Param 368 itemSelectedBackgroundColor?: ColorMetrics = undefined; 369 @Param 370 itemIconSize?: SizeT<LengthMetrics> = undefined; 371 @Param 372 itemIconFillColor?: ColorMetrics = undefined; 373 @Param 374 itemSelectedIconFillColor?: ColorMetrics = undefined; 375 @Param 376 itemSymbolFontSize?: LengthMetrics = undefined; 377 @Param 378 itemSymbolFontColor?: ColorMetrics = undefined; 379 @Param 380 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 381 @Param 382 itemMinHeight?: LengthMetrics = undefined; 383 @Param 384 itemPadding?: LocalizedPadding = undefined; 385 @Param 386 itemShadow?: ShadowOptions | ShadowStyle = undefined; 387 @Param 388 buttonBackgroundColor?: ColorMetrics = undefined; 389 @Param 390 buttonBackgroundBlurStyle?: BlurStyle = undefined; 391 @Param 392 buttonBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined; 393 @Param 394 buttonBackgroundEffect?: BackgroundEffectOptions = undefined; 395 @Param 396 buttonBorderRadius?: LengthMetrics = undefined; 397 @Param 398 buttonMinHeight?: LengthMetrics = undefined; 399 @Param 400 buttonPadding?: LengthMetrics = undefined; 401 @Param 402 languageDirection?: Direction = undefined; 403 404 build() { 405 SimpleSegmentButtonV2({ 406 theme: capsuleSimpleTheme, 407 items: this.items, 408 selectedIndex: this.selectedIndex, 409 $selectedIndex: (selectedIndex) => { 410 this.$selectedIndex?.(selectedIndex); 411 }, 412 onItemClicked: this.onItemClicked, 413 itemMinFontScale: this.itemMinFontScale, 414 itemMaxFontScale: this.itemMaxFontScale, 415 itemSpace: this.itemSpace, 416 itemFontColor: this.itemFontColor, 417 itemSelectedFontColor: this.itemSelectedFontColor, 418 itemFontSize: this.itemFontSize, 419 itemSelectedFontSize: this.itemSelectedFontSize, 420 itemFontWeight: this.itemFontWeight, 421 itemSelectedFontWeight: this.itemSelectedFontWeight, 422 itemSelectedBackgroundColor: this.itemSelectedBackgroundColor, 423 itemIconSize: this.itemIconSize, 424 itemIconFillColor: this.itemIconFillColor, 425 itemSelectedIconFillColor: this.itemSelectedIconFillColor, 426 itemSymbolFontSize: this.itemSymbolFontSize, 427 itemSymbolFontColor: this.itemSymbolFontColor, 428 itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor, 429 itemBorderRadius: this.itemBorderRadius, 430 itemMinHeight: this.itemMinHeight, 431 itemPadding: this.itemPadding, 432 itemShadow: this.itemShadow, 433 buttonBackgroundColor: this.buttonBackgroundColor, 434 buttonBackgroundBlurStyle: this.buttonBackgroundBlurStyle, 435 buttonBackgroundBlurStyleOptions: this.buttonBackgroundBlurStyleOptions, 436 buttonBackgroundEffect: this.buttonBackgroundEffect, 437 buttonBorderRadius: this.buttonBorderRadius, 438 buttonMinHeight: this.buttonMinHeight, 439 buttonPadding: this.buttonPadding, 440 languageDirection: this.languageDirection, 441 }) 442 } 443} 444 445@ComponentV2 446struct SimpleSegmentButtonV2 { 447 @Require 448 @Param 449 items: SegmentButtonV2Items; 450 @Require 451 @Param 452 selectedIndex: number; 453 @Event 454 $selectedIndex?: OnSelectedIndexChange; 455 @Require 456 @Param 457 theme: SimpleSegmentButtonV2Theme; 458 @Event 459 onItemClicked?: Callback<number>; 460 @Require 461 @Param 462 itemMinFontScale?: number | Resource = undefined; 463 @Require 464 @Param 465 itemMaxFontScale?: number | Resource = undefined; 466 @Require 467 @Param 468 itemSpace?: LengthMetrics = undefined; 469 @Require 470 @Param 471 itemFontColor?: ColorMetrics = undefined; 472 @Require 473 @Param 474 itemSelectedFontColor?: ColorMetrics = undefined; 475 @Require 476 @Param 477 itemFontSize?: LengthMetrics = undefined; 478 @Require 479 @Param 480 itemSelectedFontSize?: LengthMetrics = undefined; 481 @Require 482 @Param 483 itemFontWeight?: FontWeight = undefined; 484 @Require 485 @Param 486 itemSelectedFontWeight?: FontWeight = undefined; 487 @Require 488 @Param 489 itemBorderRadius?: LengthMetrics = undefined; 490 @Require 491 @Param 492 itemSelectedBackgroundColor?: ColorMetrics = undefined; 493 @Require 494 @Param 495 itemIconSize?: SizeT<LengthMetrics> = undefined; 496 @Require 497 @Param 498 itemIconFillColor?: ColorMetrics = undefined; 499 @Require 500 @Param 501 itemSelectedIconFillColor?: ColorMetrics = undefined; 502 @Require 503 @Param 504 itemSymbolFontSize?: LengthMetrics = undefined; 505 @Require 506 @Param 507 itemSymbolFontColor?: ColorMetrics = undefined; 508 @Require 509 @Param 510 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 511 @Require 512 @Param 513 itemMinHeight?: LengthMetrics = undefined; 514 @Require 515 @Param 516 itemPadding?: LocalizedPadding = undefined; 517 @Require 518 @Param 519 itemShadow?: ShadowOptions | ShadowStyle = undefined; 520 @Require 521 @Param 522 buttonBackgroundColor?: ColorMetrics = undefined; 523 @Require 524 @Param 525 buttonBackgroundBlurStyle?: BlurStyle = undefined; 526 @Require 527 @Param 528 buttonBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined; 529 @Require 530 @Param 531 buttonBackgroundEffect?: BackgroundEffectOptions = undefined; 532 @Require 533 @Param 534 buttonBorderRadius?: LengthMetrics = undefined; 535 @Require 536 @Param 537 buttonMinHeight?: LengthMetrics = undefined; 538 @Require 539 @Param 540 buttonPadding?: LengthMetrics = undefined; 541 @Require 542 @Param 543 languageDirection?: Direction = undefined; 544 @Local 545 itemRects: SegmentButtonV2ItemRect[] = []; 546 @Local 547 itemScale: number = 1; 548 @Local 549 hoveredItemIndex: number = -1; 550 @Local 551 mousePressedItemIndex: number = -1; 552 @Local 553 touchPressedItemIndex: number = -1; 554 private isMouseWheelScroll: boolean = false; 555 private isDragging: boolean = false; 556 private panStartGlobalX: number = 0; 557 private panStartIndex: number = -1; 558 private focusGroupId: string = GroupIdGenerator.getInstance().generate(); 559 560 @Computed 561 get normalizedSelectedIndex(): number { 562 const items = this.getItems(); 563 return normalize(this.selectedIndex, 0, items.length - 1); 564 } 565 566 @Computed 567 get selectedItemRect(): SegmentButtonV2ItemRect | undefined { 568 return this.itemRects[this.normalizedSelectedIndex]; 569 } 570 571 @LocalBuilder 572 private ContentLayer() { 573 Flex({ alignItems: ItemAlign.Stretch, space: { main: this.getItemSpace() } }) { 574 Repeat(this.getItems()) 575 .each((repeatItem: RepeatItem<SegmentButtonV2Item>) => { 576 Button({ type: ButtonType.Normal }) { 577 SegmentButtonV2ItemContent({ 578 theme: this.theme, 579 item: repeatItem.item, 580 selected: this.isSelected(repeatItem), 581 itemMinFontScale: this.itemMinFontScale, 582 itemMaxFontScale: this.itemMaxFontScale, 583 itemFontColor: this.itemFontColor, 584 itemSelectedFontColor: this.itemSelectedFontColor, 585 itemFontSize: this.itemFontSize, 586 itemSelectedFontSize: this.itemSelectedFontSize, 587 itemFontWeight: this.itemFontWeight, 588 itemSelectedFontWeight: this.itemSelectedFontWeight, 589 itemIconSize: this.itemIconSize, 590 itemIconFillColor: this.itemIconFillColor, 591 itemSelectedIconFillColor: this.itemSelectedIconFillColor, 592 itemSymbolFontSize: this.itemSymbolFontSize, 593 itemSymbolFontColor: this.itemSymbolFontColor, 594 itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor, 595 itemMinHeight: this.itemMinHeight, 596 itemPadding: this.itemPadding, 597 languageDirection: this.languageDirection, 598 hasHybrid: this.getItems().hasHybrid, 599 }) 600 } 601 .accessibilityGroup(true) 602 .accessibilitySelected(this.isSelected(repeatItem)) 603 .accessibilityText(this.getItemAccessibilityText(repeatItem)) 604 .accessibilityDescription(this.getItemAccessibilityDescription(repeatItem)) 605 .accessibilityLevel(repeatItem.item.accessibilityLevel) 606 .backgroundColor(Color.Transparent) 607 .borderRadius(this.getItemBorderRadius()) 608 .direction(this.languageDirection) 609 .enabled(repeatItem.item.enabled) 610 .focusScopePriority(this.focusGroupId, this.getFocusPriority(repeatItem)) 611 .hoverEffect(HoverEffect.None) 612 .layoutWeight(1) 613 .padding(0) 614 .scale(this.getItemScale(repeatItem.index)) 615 .stateEffect(false) 616 .onAreaChange((_, area) => { 617 this.itemRects[repeatItem.index] = { 618 size: { 619 width: area.width as number, 620 height: area.height as number, 621 }, 622 position: { 623 x: area.position.x as number, 624 y: area.position.y as number, 625 }, 626 globalPosition: { 627 x: area.globalPosition.x as number, 628 y: area.globalPosition.y as number, 629 } 630 }; 631 }) 632 .gesture( 633 TapGesture().onAction(() => { 634 this.onItemClicked?.(repeatItem.index); 635 this.updateSelectedIndex(repeatItem.index); 636 }) 637 ) 638 .onTouch((event) => { 639 if (event.type === TouchType.Down) { 640 if (this.isSelected(repeatItem)) { 641 this.updateItemScale(0.95); 642 } 643 this.updateTouchPressedItemIndex(repeatItem.index); 644 } else if ([TouchType.Up, TouchType.Cancel].includes(event.type)) { 645 this.updateItemScale(1) 646 this.updateTouchPressedItemIndex(-1); 647 } 648 }) 649 .onHover((isHover) => { 650 if (isHover) { 651 this.updateHoveredItemIndex(repeatItem.index); 652 } else { 653 this.updateHoveredItemIndex(-1); 654 } 655 }) 656 .onMouse((event) => { 657 if (event.action === MouseAction.Press) { 658 this.updateMousePressedItemIndex(repeatItem.index); 659 } else if ([MouseAction.Release, MouseAction.CANCEL].includes(event.action)) { 660 this.updateMousePressedItemIndex(-1); 661 } 662 }) 663 }) 664 .key(generateUniqueKye(this.focusGroupId)) 665 } 666 .constraintSize({ 667 minWidth: '100%', 668 minHeight: this.getButtonMinHeight() 669 }) 670 .clip(false) 671 .direction(this.languageDirection) 672 .focusScopeId(this.focusGroupId, true) 673 .padding(this.getButtonPadding()) 674 .priorityGesture( 675 PanGesture() 676 .onActionStart((event) => { 677 const finger = event.fingerList.find(Boolean); 678 if (!finger) { 679 return; 680 } 681 const index = this.getIndexByPosition(finger.globalX, finger.globalY); 682 if (!this.isItemEnabled(index)) { 683 return; 684 } 685 if (event.axisHorizontal !== 0 || event.axisVertical !== 0) { 686 this.isMouseWheelScroll = true; 687 return; 688 } 689 if (index === this.normalizedSelectedIndex) { 690 this.isDragging = true; 691 } 692 this.panStartGlobalX = finger.globalX; 693 this.panStartIndex = index; 694 }) 695 .onActionUpdate((event) => { 696 if (!this.isDragging) { 697 return; 698 } 699 const finger = event.fingerList.find(Boolean); 700 if (!finger) { 701 return; 702 } 703 const index = this.getIndexByPosition(finger.globalX, finger.globalY); 704 this.updateSelectedIndex(index); 705 }) 706 .onActionEnd((event) => { 707 if (!this.isItemEnabled(this.panStartIndex)) { 708 return; 709 } 710 // handle mouse wheel scroll event 711 if (this.isMouseWheelScroll) { 712 const offset = event.offsetX !== 0 ? event.offsetX : event.offsetY; 713 const deltaIndex = offset < 0 ? 1 : -1; 714 this.updateSelectedIndex(this.normalizedSelectedIndex + deltaIndex); 715 this.isMouseWheelScroll = false; 716 return; 717 } 718 // handle drag event 719 if (this.isDragging) { 720 this.isDragging = false; 721 return; 722 } 723 // handle swipe event 724 if (!this.isItemEnabled(this.normalizedSelectedIndex)) { 725 return; 726 } 727 const finger = event.fingerList.find(Boolean); 728 if (!finger) { 729 return; 730 } 731 let deltaIndex = finger.globalX - this.panStartGlobalX < 0 ? -1 : 1; 732 if (this.isRTL()) { 733 deltaIndex = -deltaIndex; 734 } 735 this.updateSelectedIndex(this.normalizedSelectedIndex + deltaIndex); 736 }) 737 .onActionCancel(() => { 738 this.isDragging = false; 739 this.isMouseWheelScroll = false; 740 this.panStartIndex = -1; 741 }) 742 ) 743 } 744 745 private getFocusPriority(repeatItem: RepeatItem<SegmentButtonV2Item>): FocusPriority | undefined { 746 return this.normalizedSelectedIndex === repeatItem.index ? FocusPriority.PREVIOUS : FocusPriority.AUTO; 747 } 748 749 private isItemEnabled(index: number): boolean { 750 const items = this.getItems(); 751 if (index < 0 || index >= items.length) { 752 return false; 753 } 754 return items[index].enabled; 755 } 756 757 @LocalBuilder 758 private BackplateLayer() { 759 if (this.selectedItemRect) { 760 Stack() 761 .position({ 762 x: this.selectedItemRect.position.x, 763 y: this.selectedItemRect.position.y, 764 }) 765 .backgroundColor(this.getItemSelectedBackgroundColor()) 766 .borderRadius(this.getItemBorderRadius()) 767 .scale({ x: this.itemScale, y: this.itemScale }) 768 .shadow(this.getItemBackplateShadow()) 769 .height(this.selectedItemRect.size.height) 770 .width(this.selectedItemRect.size.width) 771 } 772 } 773 774 @LocalBuilder 775 EffectLayer() { 776 Repeat(this.getItemRects()) 777 .each((repeatItem) => { 778 Stack() 779 .backgroundColor(this.getEffectBackgroundColor(repeatItem)) 780 .borderRadius(this.getItemBorderRadius()) 781 .height(repeatItem.item.size.height) 782 .position({ 783 x: repeatItem.item.position.x, 784 y: repeatItem.item.position.y 785 }) 786 .scale(this.getItemScale(repeatItem.index)) 787 .width(repeatItem.item.size.width) 788 }) 789 } 790 791 private getItemRects(): SegmentButtonV2ItemRect[] { 792 if (!this.items) { 793 return []; 794 } 795 if (this.items.length === this.itemRects.length) { 796 return this.itemRects; 797 } 798 799 return this.itemRects.slice(0, this.items.length); 800 } 801 802 build() { 803 Stack() { 804 Stack() { 805 this.EffectLayer() 806 this.BackplateLayer() 807 this.ContentLayer() 808 } 809 .borderRadius(this.getButtonBorderRadius()) 810 .backgroundBlurStyle(this.getButtonBackgroundBlurStyle(), this.getButtonBackgroundBlurStyleOptions(), 811 { disableSystemAdaptation: true }) 812 .clip(false) 813 .direction(this.languageDirection) 814 } 815 .backgroundColor(this.getButtonBackgroundColor()) 816 .backgroundEffect(this.buttonBackgroundEffect, { disableSystemAdaptation: true }) 817 .borderRadius(this.getButtonBorderRadius()) 818 .clip(false) 819 .constraintSize({ 820 minWidth: '100%', 821 minHeight: this.getButtonMinHeight() 822 }) 823 .direction(this.languageDirection) 824 } 825 826 private getItems(): SegmentButtonV2Items { 827 return this.items ?? EMPTY_ITEMS; 828 } 829 830 private getItemBackplateShadow(): ShadowOptions | ShadowStyle | undefined { 831 return this.itemShadow ?? this.theme.itemShadow 832 } 833 834 private getButtonBackgroundBlurStyle(): BlurStyle | undefined { 835 if (this.buttonBackgroundEffect) { 836 return undefined; 837 } 838 return this.buttonBackgroundBlurStyle; 839 } 840 841 private getButtonBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined { 842 if (this.buttonBackgroundEffect) { 843 return undefined; 844 } 845 return this.buttonBackgroundBlurStyleOptions; 846 } 847 848 private getItemScale(index: number): ScaleOptions { 849 const pressed: boolean = this.isPressed(index); 850 const scale: number = pressed ? 0.95 : 1; 851 return { x: scale, y: scale, }; 852 } 853 854 private isPressed(index: number): boolean { 855 return this.mousePressedItemIndex === index; 856 } 857 858 private updateHoveredItemIndex(index: number) { 859 if (index === this.hoveredItemIndex) { 860 return; 861 } 862 animateTo({ duration: 250, curve: Curve.Friction }, () => { 863 this.hoveredItemIndex = index; 864 }); 865 } 866 867 private updateMousePressedItemIndex(index: number) { 868 if (index === this.mousePressedItemIndex) { 869 return; 870 } 871 animateTo({ duration: 250, curve: Curve.Friction }, () => { 872 this.mousePressedItemIndex = index; 873 }); 874 } 875 876 private updateTouchPressedItemIndex(index: number) { 877 if (index === this.touchPressedItemIndex) { 878 return; 879 } 880 animateTo({ duration: 250, curve: Curve.Friction }, () => { 881 this.touchPressedItemIndex = index; 882 }); 883 } 884 885 private isRTL(): boolean { 886 if (this.languageDirection || this.languageDirection === Direction.Auto) { 887 return i18n.isRTL(i18n.System.getSystemLanguage()); 888 } 889 return this.languageDirection === Direction.Rtl; 890 } 891 892 private getEffectBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2ItemRect>): ResourceColor { 893 if (repeatItem.index === this.mousePressedItemIndex) { 894 return $r('sys.color.interactive_click'); 895 } 896 if (repeatItem.index === this.hoveredItemIndex) { 897 return $r('sys.color.interactive_hover'); 898 } 899 return Color.Transparent; 900 } 901 902 private getItemBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses { 903 if (this.itemBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemBorderRadius)) { 904 return LengthMetricsUtils.getInstance().stringify(this.itemBorderRadius); 905 } 906 return this.theme.itemBorderRadius; 907 908 } 909 910 private getItemSelectedBackgroundColor(): ResourceColor { 911 if (this.itemSelectedBackgroundColor) { 912 return this.itemSelectedBackgroundColor.color; 913 } 914 return this.theme.itemSelectedBackgroundColor; 915 } 916 917 getItemSpace(): LengthMetrics { 918 if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT 919 && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) { 920 return this.itemSpace; 921 } 922 return this.theme.itemSpace; 923 } 924 925 getIndexByPosition(globalX: number, globalY: number): number { 926 let index = 0; 927 while (index < this.itemRects.length) { 928 const rect = this.itemRects[index]; 929 if (this.isPointOnRect(globalX, globalY, rect)) { 930 return index; 931 } 932 ++index; 933 } 934 return -1; 935 } 936 937 private isPointOnRect(globalX: number, globalY: number, rect: SegmentButtonV2ItemRect): boolean { 938 return globalX >= rect.globalPosition.x && globalX <= rect.globalPosition.x + rect.size.width && 939 globalY >= rect.globalPosition.y && globalY <= rect.globalPosition.y + rect.size.height; 940 } 941 942 private updateSelectedIndex(selectedIndex: number) { 943 if (!this.isItemEnabled(selectedIndex) || selectedIndex === this.selectedIndex 944 ) { 945 return; 946 } 947 this.getUIContext().animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => { 948 this.$selectedIndex?.(selectedIndex); 949 }); 950 } 951 952 private updateItemScale(scale: number) { 953 if (this.itemScale === scale) { 954 return; 955 } 956 this.getUIContext().animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 957 this.itemScale = scale; 958 }); 959 } 960 961 private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 962 return repeatItem.item.accessibilityDescription as ESObject as string; 963 } 964 965 private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 966 return repeatItem.item.accessibilityText as ESObject as string; 967 } 968 969 private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean | undefined { 970 return repeatItem.index === this.normalizedSelectedIndex; 971 } 972 973 private getButtonPadding(): Length | Padding | LocalizedPadding { 974 if (this.buttonPadding && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonPadding)) { 975 return LengthMetricsUtils.getInstance().stringify(this.buttonPadding); 976 } 977 return this.theme.buttonPadding; 978 } 979 980 private getButtonBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses { 981 if (this.buttonBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonBorderRadius)) { 982 return LengthMetricsUtils.getInstance().stringify(this.buttonBorderRadius); 983 } 984 return this.theme.buttonBorderRadius; 985 } 986 987 private getButtonBackgroundColor(): ResourceColor { 988 if (this.buttonBackgroundColor) { 989 return this.buttonBackgroundColor.color; 990 } 991 return this.theme.buttonBackgroundColor; 992 } 993 994 private getButtonMinHeight(): Dimension { 995 if (this.buttonMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonMinHeight)) { 996 return LengthMetricsUtils.getInstance().stringify(this.buttonMinHeight); 997 } 998 const items = this.getItems(); 999 return items.hasHybrid ? this.theme.hybridButtonMinHeight : this.theme.buttonMinHeight; 1000 } 1001} 1002 1003interface MultiplySegmentButtonV2Theme extends SegmentButtonV2ContentTheme { 1004 itemBackgroundColor: ResourceColor; 1005 itemSelectedBackgroundColor: ResourceColor; 1006 itemBorderRadius: Resource; 1007} 1008 1009const multiplyCapsuleTheme: MultiplySegmentButtonV2Theme = { 1010 itemBorderRadius: $r('sys.float.segment_button_v2_multi_corner_radius'), 1011 itemBackgroundColor: $r('sys.color.segment_button_v2_multi_capsule_button_background'), 1012 itemSelectedBackgroundColor: $r('sys.color.comp_background_emphasize'), 1013 itemSpace: LengthMetrics.vp(1), 1014 itemFontColor: $r('sys.color.font_secondary'), 1015 itemSelectedFontColor: $r('sys.color.font_on_primary'), 1016 itemFontWeight: FontWeight.Medium, 1017 itemSelectedFontWeight: FontWeight.Medium, 1018 itemIconFillColor: $r('sys.color.icon_secondary'), 1019 itemSelectedIconFillColor: $r('sys.color.font_on_primary'), 1020 itemSymbolFontColor: $r('sys.color.font_secondary'), 1021 itemSelectedSymbolFontColor: $r('sys.color.font_on_primary'), 1022 itemFontSize: $r('sys.float.ohos_id_text_size_button2'), 1023 itemIconSize: 24, 1024 itemSymbolFontSize: 20, 1025 itemPadding: { 1026 top: LengthMetrics.resource($r('sys.float.padding_level2')), 1027 bottom: LengthMetrics.resource($r('sys.float.padding_level2')), 1028 start: LengthMetrics.resource($r('sys.float.padding_level4')), 1029 end: LengthMetrics.resource($r('sys.float.padding_level4')), 1030 }, 1031 itemMinHeight: $r('sys.float.segment_button_v2_multi_singleline_height'), 1032 hybridItemMinHeight: $r('sys.float.segment_button_v2_multi_doubleline_height'), 1033 itemMaxFontScale: SMALLEST_MAX_FONT_SCALE, 1034 itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE, 1035 itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE, 1036 itemMinFontScale: SMALLEST_MIN_FONT_SCALE, 1037 itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE, 1038 itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE, 1039}; 1040 1041@ComponentV2 1042export struct MultiCapsuleSegmentButtonV2 { 1043 @Require 1044 @Param 1045 items: SegmentButtonV2Items; 1046 @Require 1047 @Param 1048 selectedIndexes: number[]; 1049 @Event 1050 $selectedIndexes: OnSelectedIndexesChange; 1051 @Event 1052 onItemClicked?: Callback<number>; 1053 @Param 1054 itemMinFontScale?: number | Resource = undefined; 1055 @Param 1056 itemMaxFontScale?: number | Resource = undefined; 1057 @Param 1058 itemSpace?: LengthMetrics = undefined; 1059 @Param 1060 itemFontColor?: ColorMetrics = undefined; 1061 @Param 1062 itemSelectedFontColor?: ColorMetrics = undefined; 1063 @Param 1064 itemFontSize?: LengthMetrics = undefined; 1065 @Param 1066 itemSelectedFontSize?: LengthMetrics = undefined; 1067 @Param 1068 itemFontWeight?: FontWeight = undefined; 1069 @Param 1070 itemSelectedFontWeight?: FontWeight = undefined; 1071 @Param 1072 itemBorderRadius?: LengthMetrics = undefined; 1073 @Param 1074 itemBackgroundColor?: ColorMetrics = undefined; 1075 @Param 1076 itemBackgroundEffect?: BackgroundEffectOptions = undefined; 1077 @Param 1078 itemBackgroundBlurStyle?: BlurStyle = undefined; 1079 @Param 1080 itemBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined; 1081 @Param 1082 itemSelectedBackgroundColor?: ColorMetrics = undefined; 1083 @Param 1084 itemIconSize?: SizeT<LengthMetrics> = undefined; 1085 @Param 1086 itemIconFillColor?: ColorMetrics = undefined; 1087 @Param 1088 itemSelectedIconFillColor?: ColorMetrics = undefined; 1089 @Param 1090 itemSymbolFontSize?: LengthMetrics = undefined; 1091 @Param 1092 itemSymbolFontColor?: ColorMetrics = undefined; 1093 @Param 1094 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 1095 @Param 1096 itemMinHeight?: LengthMetrics = undefined; 1097 @Param 1098 itemPadding?: LocalizedPadding = undefined; 1099 @Param 1100 languageDirection?: Direction = undefined; 1101 private theme: MultiplySegmentButtonV2Theme = multiplyCapsuleTheme; 1102 private focusGroupId: string = GroupIdGenerator.getInstance().generate(); 1103 1104 build() { 1105 Flex({ alignItems: ItemAlign.Stretch, space: { main: this.getItemSpace() } }) { 1106 Repeat(this.getItems()) 1107 .each((repeatItem: RepeatItem<SegmentButtonV2Item>) => { 1108 Button({ type: ButtonType.Normal }) { 1109 SegmentButtonV2ItemContent({ 1110 theme: this.theme, 1111 item: repeatItem.item, 1112 selected: this.isSelected(repeatItem), 1113 hasHybrid: this.getItems().hasHybrid, 1114 itemMinFontScale: this.itemMinFontScale, 1115 itemMaxFontScale: this.itemMaxFontScale, 1116 itemFontColor: this.itemFontColor, 1117 itemSelectedFontColor: this.itemSelectedFontColor, 1118 itemFontSize: this.itemFontSize, 1119 itemSelectedFontSize: this.itemSelectedFontSize, 1120 itemFontWeight: this.itemFontWeight, 1121 itemSelectedFontWeight: this.itemSelectedFontWeight, 1122 itemIconSize: this.itemIconSize, 1123 itemIconFillColor: this.itemIconFillColor, 1124 itemSelectedIconFillColor: this.itemSelectedIconFillColor, 1125 itemSymbolFontSize: this.itemSymbolFontSize, 1126 itemSymbolFontColor: this.itemSymbolFontColor, 1127 itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor, 1128 itemMinHeight: this.itemMinHeight, 1129 itemPadding: this.itemPadding, 1130 languageDirection: this.languageDirection, 1131 }) 1132 .borderRadius(this.getItemButtonBorderRadius(repeatItem)) 1133 .backgroundBlurStyle(this.getItemBackgroundBlurStyle(), this.getItemBackgroundBlurStyleOptions(), 1134 { disableSystemAdaptation: true }) 1135 .direction(this.languageDirection) 1136 } 1137 .accessibilityGroup(true) 1138 .accessibilityChecked(this.isSelected(repeatItem)) 1139 .accessibilityText(this.getItemAccessibilityText(repeatItem)) 1140 .accessibilityDescription(this.getItemAccessibilityDescription(repeatItem)) 1141 .accessibilityLevel(repeatItem.item.accessibilityLevel) 1142 .backgroundColor(this.getItemBackgroundColor(repeatItem)) 1143 .backgroundEffect(this.itemBackgroundEffect, { disableSystemAdaptation: true }) 1144 .borderRadius(this.getItemButtonBorderRadius(repeatItem)) 1145 .constraintSize({ minHeight: this.getItemMinHeight() }) 1146 .direction(this.languageDirection) 1147 .enabled(repeatItem.item.enabled) 1148 .focusScopePriority(this.focusGroupId, this.getFocusPriority(repeatItem)) 1149 .layoutWeight(1) 1150 .padding(0) 1151 .onClick(() => { 1152 this.onItemClicked?.(repeatItem.index); 1153 let selection: number[]; 1154 const items = this.getItems(); 1155 const selectedIndexes = this.selectedIndexes ?? []; 1156 if (this.isSelected(repeatItem)) { 1157 selection = selectedIndexes.filter((index) => { 1158 if (index < 0 || index > items.length - 1) { 1159 return false; 1160 } 1161 return index !== repeatItem.index; 1162 }); 1163 } else { 1164 selection = selectedIndexes 1165 .filter((index) => index >= 0 && index <= items.length - 1) 1166 .concat(repeatItem.index); 1167 } 1168 this.$selectedIndexes(selection); 1169 }) 1170 }) 1171 .key(generateUniqueKye(this.focusGroupId)) 1172 } 1173 .clip(false) 1174 .direction(this.languageDirection) 1175 .focusScopeId(this.focusGroupId, true) 1176 } 1177 1178 private getFocusPriority(repeatItem: RepeatItem<SegmentButtonV2Item>): FocusPriority | undefined { 1179 return Math.min(...this.selectedIndexes) === repeatItem.index ? FocusPriority.PREVIOUS : FocusPriority.AUTO; 1180 } 1181 1182 getItems(): SegmentButtonV2Items { 1183 return this.items ?? EMPTY_ITEMS; 1184 } 1185 1186 getItemBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined { 1187 if (this.itemBackgroundEffect) { 1188 return undefined; 1189 } 1190 return this.itemBackgroundBlurStyleOptions; 1191 } 1192 1193 getItemBackgroundBlurStyle(): BlurStyle | undefined { 1194 if (this.itemBackgroundEffect) { 1195 return undefined; 1196 } 1197 return this.itemBackgroundBlurStyle; 1198 } 1199 1200 private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 1201 return repeatItem.item.accessibilityDescription as ESObject as string; 1202 } 1203 1204 private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 1205 return repeatItem.item.accessibilityText as ESObject as string; 1206 } 1207 1208 private getItemSpace(): LengthMetrics { 1209 if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT 1210 && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) { 1211 return this.itemSpace; 1212 } 1213 return this.theme.itemSpace; 1214 } 1215 1216 private getItemMinHeight(): Length | undefined { 1217 if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) { 1218 return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight); 1219 } 1220 return this.theme.itemMinHeight; 1221 } 1222 1223 private getItemBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2Item>): ResourceColor { 1224 if (this.isSelected(repeatItem)) { 1225 return this.itemSelectedBackgroundColor?.color ?? this.theme.itemSelectedBackgroundColor; 1226 } 1227 return this.itemBackgroundColor?.color ?? this.theme.itemBackgroundColor; 1228 } 1229 1230 private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean { 1231 const selectedIndexes = this.selectedIndexes ?? []; 1232 return selectedIndexes.includes(repeatItem.index); 1233 } 1234 1235 private getItemButtonBorderRadius(repeatItem: RepeatItem<SegmentButtonV2Item>): LocalizedBorderRadiuses { 1236 const items = this.getItems(); 1237 const noneBorderRadius: LengthMetrics = LengthMetrics.vp(0); 1238 const borderRadiuses: LocalizedBorderRadiuses = { 1239 topStart: noneBorderRadius, 1240 bottomStart: noneBorderRadius, 1241 topEnd: noneBorderRadius, 1242 bottomEnd: noneBorderRadius, 1243 }; 1244 if (repeatItem.index === 0) { 1245 const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius); 1246 borderRadiuses.topStart = borderRadius; 1247 borderRadiuses.bottomStart = borderRadius; 1248 1249 } 1250 if (repeatItem.index === items.length - 1) { 1251 const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius); 1252 borderRadiuses.topEnd = borderRadius; 1253 borderRadiuses.bottomEnd = borderRadius; 1254 } 1255 return borderRadiuses; 1256 } 1257} 1258 1259@ComponentV2 1260struct SegmentButtonV2ItemContent { 1261 @Require 1262 @Param 1263 hasHybrid: boolean; 1264 @Require 1265 @Param 1266 item: SegmentButtonV2Item; 1267 @Require 1268 @Param 1269 selected: boolean; 1270 @Require 1271 @Param 1272 theme: SegmentButtonV2ContentTheme; 1273 @Require 1274 @Param 1275 itemMinFontScale?: number | Resource = undefined; 1276 @Require 1277 @Param 1278 itemMaxFontScale?: number | Resource = undefined; 1279 @Require 1280 @Param 1281 itemFontColor?: ColorMetrics = undefined; 1282 @Require 1283 @Param 1284 itemSelectedFontColor?: ColorMetrics = undefined; 1285 @Require 1286 @Param 1287 itemFontSize?: LengthMetrics = undefined; 1288 @Require 1289 @Param 1290 itemSelectedFontSize?: LengthMetrics = undefined; 1291 @Require 1292 @Param 1293 itemFontWeight?: FontWeight = undefined; 1294 @Require 1295 @Param 1296 itemSelectedFontWeight?: FontWeight = undefined; 1297 @Require 1298 @Param 1299 itemIconSize?: SizeT<LengthMetrics> = undefined; 1300 @Require 1301 @Param 1302 itemIconFillColor?: ColorMetrics = undefined; 1303 @Require 1304 @Param 1305 itemSelectedIconFillColor?: ColorMetrics = undefined; 1306 @Require 1307 @Param 1308 itemSymbolFontSize?: LengthMetrics = undefined; 1309 @Require 1310 @Param 1311 itemSymbolFontColor?: ColorMetrics = undefined; 1312 @Require 1313 @Param 1314 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 1315 @Require 1316 @Param 1317 itemMinHeight?: LengthMetrics = undefined; 1318 @Require 1319 @Param 1320 itemPadding?: LocalizedPadding = undefined; 1321 @Require 1322 @Param 1323 languageDirection?: Direction = undefined; 1324 1325 build() { 1326 Column({ space: 2 }) { 1327 if (this.item.symbol || this.item.symbolModifier) { 1328 SymbolGlyph(this.item.symbol) 1329 .fontSize(this.getSymbolFontSize()) 1330 .fontColor([this.getItemSymbolFillColor()]) 1331 .direction(this.languageDirection) 1332 .attributeModifier(this.item.symbolModifier) 1333 } else if (this.item.icon) { 1334 Image(this.item.icon) 1335 .fillColor(this.getItemIconFillColor()) 1336 .width(this.getItemIconWidth()) 1337 .height(this.getItemIconHeight()) 1338 .direction(this.languageDirection) 1339 .draggable(false) 1340 .attributeModifier(this.item.iconModifier) 1341 } 1342 1343 if (this.item.text) { 1344 Text(this.item.text) 1345 .direction(this.languageDirection) 1346 .fontSize(this.getItemFontSize()) 1347 .fontColor(this.getItemFontColor()) 1348 .fontWeight(this.getItemFontWeight()) 1349 .textOverflow({ overflow: TextOverflow.Ellipsis }) 1350 .maxLines(1) 1351 .maxFontScale(this.getItemMaxFontScale()) 1352 .minFontScale(this.getItemMinFontScale()) 1353 .attributeModifier(this.item.textModifier) 1354 } 1355 } 1356 .constraintSize({ minHeight: this.getItemMinHeight(), minWidth: '100%' }) 1357 .direction(this.languageDirection) 1358 .justifyContent(FlexAlign.Center) 1359 .padding(this.getItemPadding()) 1360 } 1361 1362 private getItemFontWeight(): string | number | FontWeight { 1363 if (this.selected) { 1364 return this.itemSelectedFontWeight ?? this.theme.itemSelectedFontWeight; 1365 } 1366 return this.itemFontWeight ?? this.theme.itemFontWeight; 1367 } 1368 1369 getItemSymbolFillColor(): ResourceColor { 1370 if (this.selected) { 1371 return this.itemSelectedSymbolFontColor?.color ?? this.theme.itemSelectedSymbolFontColor; 1372 } 1373 return this.itemSymbolFontColor?.color ?? this.theme.itemSymbolFontColor; 1374 } 1375 1376 private getSymbolFontSize(): Dimension { 1377 if (this.itemSymbolFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSymbolFontSize) && 1378 this.itemSymbolFontSize.unit !== LengthUnit.PERCENT) { 1379 return LengthMetricsUtils.getInstance().stringify(this.itemSymbolFontSize); 1380 } 1381 return this.theme.itemSymbolFontSize; 1382 } 1383 1384 private getItemMaxFontScale() { 1385 if (typeof this.itemMaxFontScale === 'number') { 1386 return normalize(this.itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest); 1387 } 1388 if (typeof this.itemMaxFontScale === 'object') { 1389 const itemMaxFontScale: number = 1390 parseNumericResource(this.getUIContext(), this.itemMaxFontScale) ?? SMALLEST_MAX_FONT_SCALE; 1391 return normalize(itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest); 1392 } 1393 return SMALLEST_MAX_FONT_SCALE; 1394 } 1395 1396 private getItemMinFontScale() { 1397 if (typeof this.itemMinFontScale === 'number') { 1398 return normalize(this.itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest); 1399 } 1400 if (typeof this.itemMinFontScale === 'object') { 1401 const itemMinFontScale = 1402 parseNumericResource(this.getUIContext(), this.itemMinFontScale) ?? SMALLEST_MIN_FONT_SCALE; 1403 return normalize(itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest); 1404 } 1405 return SMALLEST_MIN_FONT_SCALE; 1406 } 1407 1408 private getItemPadding(): LocalizedPadding | Length | Padding { 1409 const itemPadding: LocalizedPadding = { 1410 top: this.theme.itemPadding.top, 1411 bottom: this.theme.itemPadding.bottom, 1412 start: this.theme.itemPadding.start, 1413 end: this.theme.itemPadding.end, 1414 }; 1415 1416 if (this.itemPadding?.top && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.top)) { 1417 itemPadding.top = this.itemPadding.top; 1418 } 1419 1420 if (this.itemPadding?.bottom && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.bottom)) { 1421 itemPadding.bottom = this.itemPadding.bottom; 1422 } 1423 1424 if (this.itemPadding?.start && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.start)) { 1425 itemPadding.start = this.itemPadding.start; 1426 } 1427 1428 if (this.itemPadding?.end && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.end)) { 1429 itemPadding.end = this.itemPadding.end; 1430 } 1431 1432 return itemPadding; 1433 } 1434 1435 private getItemMinHeight(): Length | undefined { 1436 if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) { 1437 return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight); 1438 } 1439 return this.hasHybrid ? this.theme.hybridItemMinHeight : this.theme.itemMinHeight; 1440 } 1441 1442 private getItemFontColor(): ResourceColor { 1443 if (this.selected) { 1444 if (this.itemSelectedFontColor) { 1445 return this.itemSelectedFontColor.color; 1446 } 1447 return this.theme.itemSelectedFontColor; 1448 } 1449 if (this.itemFontColor) { 1450 return this.itemFontColor.color; 1451 } 1452 return this.theme.itemFontColor; 1453 } 1454 1455 private getItemFontSize(): Length { 1456 if (this.selected) { 1457 if (this.itemSelectedFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSelectedFontSize) && 1458 this.itemSelectedFontSize.unit !== LengthUnit.PERCENT) { 1459 return LengthMetricsUtils.getInstance().stringify(this.itemSelectedFontSize); 1460 } 1461 return this.theme.itemFontSize; 1462 } 1463 if (this.itemFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemFontSize) && 1464 this.itemFontSize.unit !== LengthUnit.PERCENT) { 1465 return LengthMetricsUtils.getInstance().stringify(this.itemFontSize); 1466 } 1467 return this.theme.itemFontSize; 1468 } 1469 1470 private getItemIconHeight(): Length { 1471 if (this.itemIconSize?.height && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.height)) { 1472 return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.height); 1473 } 1474 return this.theme.itemIconSize; 1475 } 1476 1477 private getItemIconWidth(): Length { 1478 if (this.itemIconSize?.width && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.width)) { 1479 return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.width); 1480 } 1481 return this.theme.itemIconSize; 1482 } 1483 1484 private getItemIconFillColor(): ResourceColor { 1485 if (this.selected) { 1486 if (this.itemSelectedIconFillColor) { 1487 return this.itemSelectedIconFillColor.color; 1488 } 1489 return this.theme.itemSelectedIconFillColor; 1490 } 1491 if (this.itemIconFillColor) { 1492 return this.itemIconFillColor.color; 1493 } 1494 return this.theme.itemIconFillColor; 1495 } 1496} 1497 1498class LengthMetricsUtils { 1499 private static instance?: LengthMetricsUtils; 1500 1501 private constructor() { 1502 } 1503 1504 public static getInstance(): LengthMetricsUtils { 1505 if (!LengthMetricsUtils.instance) { 1506 LengthMetricsUtils.instance = new LengthMetricsUtils(); 1507 } 1508 return LengthMetricsUtils.instance; 1509 } 1510 1511 stringify(metrics: LengthMetrics): Dimension { 1512 switch (metrics.unit) { 1513 case LengthUnit.PX: 1514 return `${metrics.value}px`; 1515 case LengthUnit.VP: 1516 return `${metrics.value}vp`; 1517 case LengthUnit.FP: 1518 return `${metrics.value}fp`; 1519 case LengthUnit.PERCENT: 1520 return `${metrics.value}%`; 1521 case LengthUnit.LPX: 1522 return `${metrics.value}lpx`; 1523 } 1524 } 1525 1526 isNaturalNumber(metrics: LengthMetrics): boolean { 1527 return metrics.value >= 0; 1528 } 1529} 1530 1531function parseNumericResource(context: UIContext, resource: Resource): number | undefined { 1532 const resourceManager = context.getHostContext()?.resourceManager; 1533 if (!resourceManager) { 1534 return undefined; 1535 } 1536 try { 1537 return resourceManager.getNumber(resource); 1538 } catch (err) { 1539 // todo log err 1540 return undefined; 1541 } 1542} 1543 1544function normalize(value: number, min: number, max: number): number { 1545 return Math.min(Math.max(value, min), max); 1546} 1547 1548function generateUniqueKye(groupId: string) { 1549 return (item: SegmentButtonV2Item, index: number): string => { 1550 let key = groupId; 1551 if (item.text) { 1552 if (typeof item.text === 'string') { 1553 key += item.text; 1554 } else { 1555 key += getResourceUniqueId(item.text); 1556 } 1557 } 1558 if (item.icon) { 1559 if (typeof item.icon === 'string') { 1560 key += item.icon; 1561 } else { 1562 key += getResourceUniqueId(item.icon); 1563 } 1564 } 1565 if (item.symbol) { 1566 key += getResourceUniqueId(item.symbol); 1567 } 1568 return key; 1569 } 1570} 1571 1572function getResourceUniqueId(resource: Resource): string { 1573 if (resource.id !== -1) { 1574 return `${resource.id}`; 1575 } else { 1576 return JSON.stringify(resource); 1577 } 1578} 1579 1580class GroupIdGenerator { 1581 private static instance: GroupIdGenerator | null = null; 1582 private id: number = 0; 1583 1584 private constructor() { 1585 } 1586 1587 public static getInstance(): GroupIdGenerator { 1588 if (!GroupIdGenerator.instance) { 1589 GroupIdGenerator.instance = new GroupIdGenerator(); 1590 } 1591 return GroupIdGenerator.instance; 1592 } 1593 1594 public generate(): string { 1595 return util.generateRandomUUID() || `SegmentButton-${this.id++}`; 1596 } 1597} 1598