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 = util.generateRandomUUID(); 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 .clip(false) 812 .direction(this.languageDirection) 813 } 814 .backgroundColor(this.getButtonBackgroundColor()) 815 .backgroundEffect(this.buttonBackgroundEffect) 816 .borderRadius(this.getButtonBorderRadius()) 817 .clip(false) 818 .constraintSize({ 819 minWidth: '100%', 820 minHeight: this.getButtonMinHeight() 821 }) 822 .direction(this.languageDirection) 823 } 824 825 private getItems(): SegmentButtonV2Items { 826 return this.items ?? EMPTY_ITEMS; 827 } 828 829 private getItemBackplateShadow(): ShadowOptions | ShadowStyle | undefined { 830 return this.itemShadow ?? this.theme.itemShadow 831 } 832 833 private getButtonBackgroundBlurStyle(): BlurStyle | undefined { 834 if (this.buttonBackgroundEffect) { 835 return undefined; 836 } 837 return this.buttonBackgroundBlurStyle; 838 } 839 840 private getButtonBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined { 841 if (this.buttonBackgroundEffect) { 842 return undefined; 843 } 844 return this.buttonBackgroundBlurStyleOptions; 845 } 846 847 private getItemScale(index: number): ScaleOptions { 848 const pressed: boolean = this.isPressed(index); 849 const scale: number = pressed ? 0.95 : 1; 850 return { x: scale, y: scale, }; 851 } 852 853 private isPressed(index: number): boolean { 854 return this.mousePressedItemIndex === index; 855 } 856 857 private updateHoveredItemIndex(index: number) { 858 if (index === this.hoveredItemIndex) { 859 return; 860 } 861 animateTo({ duration: 250, curve: Curve.Friction }, () => { 862 this.hoveredItemIndex = index; 863 }); 864 } 865 866 private updateMousePressedItemIndex(index: number) { 867 if (index === this.mousePressedItemIndex) { 868 return; 869 } 870 animateTo({ duration: 250, curve: Curve.Friction }, () => { 871 this.mousePressedItemIndex = index; 872 }); 873 } 874 875 private updateTouchPressedItemIndex(index: number) { 876 if (index === this.touchPressedItemIndex) { 877 return; 878 } 879 animateTo({ duration: 250, curve: Curve.Friction }, () => { 880 this.touchPressedItemIndex = index; 881 }); 882 } 883 884 private isRTL(): boolean { 885 if (this.languageDirection || this.languageDirection === Direction.Auto) { 886 return i18n.isRTL(i18n.System.getSystemLanguage()); 887 } 888 return this.languageDirection === Direction.Rtl; 889 } 890 891 private getEffectBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2ItemRect>): ResourceColor { 892 if (repeatItem.index === this.mousePressedItemIndex) { 893 return $r('sys.color.interactive_click'); 894 } 895 if (repeatItem.index === this.hoveredItemIndex) { 896 return $r('sys.color.interactive_hover'); 897 } 898 return Color.Transparent; 899 } 900 901 private getItemBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses { 902 if (this.itemBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemBorderRadius)) { 903 return LengthMetricsUtils.getInstance().stringify(this.itemBorderRadius); 904 } 905 return this.theme.itemBorderRadius; 906 907 } 908 909 private getItemSelectedBackgroundColor(): ResourceColor { 910 if (this.itemSelectedBackgroundColor) { 911 return this.itemSelectedBackgroundColor.color; 912 } 913 return this.theme.itemSelectedBackgroundColor; 914 } 915 916 getItemSpace(): LengthMetrics { 917 if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT 918 && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) { 919 return this.itemSpace; 920 } 921 return this.theme.itemSpace; 922 } 923 924 getIndexByPosition(globalX: number, globalY: number): number { 925 let index = 0; 926 while (index < this.itemRects.length) { 927 const rect = this.itemRects[index]; 928 if (this.isPointOnRect(globalX, globalY, rect)) { 929 return index; 930 } 931 ++index; 932 } 933 return -1; 934 } 935 936 private isPointOnRect(globalX: number, globalY: number, rect: SegmentButtonV2ItemRect): boolean { 937 return globalX >= rect.globalPosition.x && globalX <= rect.globalPosition.x + rect.size.width && 938 globalY >= rect.globalPosition.y && globalY <= rect.globalPosition.y + rect.size.height; 939 } 940 941 private updateSelectedIndex(selectedIndex: number) { 942 if (!this.isItemEnabled(selectedIndex) || selectedIndex === this.selectedIndex 943 ) { 944 return; 945 } 946 this.getUIContext().animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => { 947 this.$selectedIndex?.(selectedIndex); 948 }); 949 } 950 951 private updateItemScale(scale: number) { 952 if (this.itemScale === scale) { 953 return; 954 } 955 this.getUIContext().animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 956 this.itemScale = scale; 957 }); 958 } 959 960 private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 961 return repeatItem.item.accessibilityDescription as ESObject as string; 962 } 963 964 private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 965 return repeatItem.item.accessibilityText as ESObject as string; 966 } 967 968 private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean | undefined { 969 return repeatItem.index === this.normalizedSelectedIndex; 970 } 971 972 private getButtonPadding(): Length | Padding | LocalizedPadding { 973 if (this.buttonPadding && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonPadding)) { 974 return LengthMetricsUtils.getInstance().stringify(this.buttonPadding); 975 } 976 return this.theme.buttonPadding; 977 } 978 979 private getButtonBorderRadius(): Length | BorderRadiuses | LocalizedBorderRadiuses { 980 if (this.buttonBorderRadius && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonBorderRadius)) { 981 return LengthMetricsUtils.getInstance().stringify(this.buttonBorderRadius); 982 } 983 return this.theme.buttonBorderRadius; 984 } 985 986 private getButtonBackgroundColor(): ResourceColor { 987 if (this.buttonBackgroundColor) { 988 return this.buttonBackgroundColor.color; 989 } 990 return this.theme.buttonBackgroundColor; 991 } 992 993 private getButtonMinHeight(): Dimension { 994 if (this.buttonMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.buttonMinHeight)) { 995 return LengthMetricsUtils.getInstance().stringify(this.buttonMinHeight); 996 } 997 const items = this.getItems(); 998 return items.hasHybrid ? this.theme.hybridButtonMinHeight : this.theme.buttonMinHeight; 999 } 1000} 1001 1002interface MultiplySegmentButtonV2Theme extends SegmentButtonV2ContentTheme { 1003 itemBackgroundColor: ResourceColor; 1004 itemSelectedBackgroundColor: ResourceColor; 1005 itemBorderRadius: Resource; 1006} 1007 1008const multiplyCapsuleTheme: MultiplySegmentButtonV2Theme = { 1009 itemBorderRadius: $r('sys.float.segment_button_v2_multi_corner_radius'), 1010 itemBackgroundColor: $r('sys.color.segment_button_v2_multi_capsule_button_background'), 1011 itemSelectedBackgroundColor: $r('sys.color.comp_background_emphasize'), 1012 itemSpace: LengthMetrics.vp(1), 1013 itemFontColor: $r('sys.color.font_secondary'), 1014 itemSelectedFontColor: $r('sys.color.font_on_primary'), 1015 itemFontWeight: FontWeight.Medium, 1016 itemSelectedFontWeight: FontWeight.Medium, 1017 itemIconFillColor: $r('sys.color.icon_secondary'), 1018 itemSelectedIconFillColor: $r('sys.color.font_on_primary'), 1019 itemSymbolFontColor: $r('sys.color.font_secondary'), 1020 itemSelectedSymbolFontColor: $r('sys.color.font_on_primary'), 1021 itemFontSize: $r('sys.float.ohos_id_text_size_button2'), 1022 itemIconSize: 24, 1023 itemSymbolFontSize: 20, 1024 itemPadding: { 1025 top: LengthMetrics.resource($r('sys.float.padding_level2')), 1026 bottom: LengthMetrics.resource($r('sys.float.padding_level2')), 1027 start: LengthMetrics.resource($r('sys.float.padding_level4')), 1028 end: LengthMetrics.resource($r('sys.float.padding_level4')), 1029 }, 1030 itemMinHeight: $r('sys.float.segment_button_v2_multi_singleline_height'), 1031 hybridItemMinHeight: $r('sys.float.segment_button_v2_multi_doubleline_height'), 1032 itemMaxFontScale: SMALLEST_MAX_FONT_SCALE, 1033 itemMaxFontScaleSmallest: SMALLEST_MAX_FONT_SCALE, 1034 itemMaxFontScaleLargest: LARGEST_MAX_FONT_SCALE, 1035 itemMinFontScale: SMALLEST_MIN_FONT_SCALE, 1036 itemMinFontScaleSmallest: SMALLEST_MIN_FONT_SCALE, 1037 itemMinFontScaleLargest: LARGEST_MIN_FONT_SCALE, 1038}; 1039 1040@ComponentV2 1041export struct MultiCapsuleSegmentButtonV2 { 1042 @Require 1043 @Param 1044 items: SegmentButtonV2Items; 1045 @Require 1046 @Param 1047 selectedIndexes: number[]; 1048 @Event 1049 $selectedIndexes: OnSelectedIndexesChange; 1050 @Event 1051 onItemClicked?: Callback<number>; 1052 @Param 1053 itemMinFontScale?: number | Resource = undefined; 1054 @Param 1055 itemMaxFontScale?: number | Resource = undefined; 1056 @Param 1057 itemSpace?: LengthMetrics = undefined; 1058 @Param 1059 itemFontColor?: ColorMetrics = undefined; 1060 @Param 1061 itemSelectedFontColor?: ColorMetrics = undefined; 1062 @Param 1063 itemFontSize?: LengthMetrics = undefined; 1064 @Param 1065 itemSelectedFontSize?: LengthMetrics = undefined; 1066 @Param 1067 itemFontWeight?: FontWeight = undefined; 1068 @Param 1069 itemSelectedFontWeight?: FontWeight = undefined; 1070 @Param 1071 itemBorderRadius?: LengthMetrics = undefined; 1072 @Param 1073 itemBackgroundColor?: ColorMetrics = undefined; 1074 @Param 1075 itemBackgroundEffect?: BackgroundEffectOptions = undefined; 1076 @Param 1077 itemBackgroundBlurStyle?: BlurStyle = undefined; 1078 @Param 1079 itemBackgroundBlurStyleOptions?: BackgroundBlurStyleOptions = undefined; 1080 @Param 1081 itemSelectedBackgroundColor?: ColorMetrics = undefined; 1082 @Param 1083 itemIconSize?: SizeT<LengthMetrics> = undefined; 1084 @Param 1085 itemIconFillColor?: ColorMetrics = undefined; 1086 @Param 1087 itemSelectedIconFillColor?: ColorMetrics = undefined; 1088 @Param 1089 itemSymbolFontSize?: LengthMetrics = undefined; 1090 @Param 1091 itemSymbolFontColor?: ColorMetrics = undefined; 1092 @Param 1093 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 1094 @Param 1095 itemMinHeight?: LengthMetrics = undefined; 1096 @Param 1097 itemPadding?: LocalizedPadding = undefined; 1098 @Param 1099 languageDirection?: Direction = undefined; 1100 private theme: MultiplySegmentButtonV2Theme = multiplyCapsuleTheme; 1101 private focusGroupId: string = util.generateRandomUUID(); 1102 1103 build() { 1104 Flex({ alignItems: ItemAlign.Stretch, space: { main: this.getItemSpace() } }) { 1105 Repeat(this.getItems()) 1106 .each((repeatItem: RepeatItem<SegmentButtonV2Item>) => { 1107 Button({ type: ButtonType.Normal }) { 1108 SegmentButtonV2ItemContent({ 1109 theme: this.theme, 1110 item: repeatItem.item, 1111 selected: this.isSelected(repeatItem), 1112 hasHybrid: this.getItems().hasHybrid, 1113 itemMinFontScale: this.itemMinFontScale, 1114 itemMaxFontScale: this.itemMaxFontScale, 1115 itemFontColor: this.itemFontColor, 1116 itemSelectedFontColor: this.itemSelectedFontColor, 1117 itemFontSize: this.itemFontSize, 1118 itemSelectedFontSize: this.itemSelectedFontSize, 1119 itemFontWeight: this.itemFontWeight, 1120 itemSelectedFontWeight: this.itemSelectedFontWeight, 1121 itemIconSize: this.itemIconSize, 1122 itemIconFillColor: this.itemIconFillColor, 1123 itemSelectedIconFillColor: this.itemSelectedIconFillColor, 1124 itemSymbolFontSize: this.itemSymbolFontSize, 1125 itemSymbolFontColor: this.itemSymbolFontColor, 1126 itemSelectedSymbolFontColor: this.itemSelectedSymbolFontColor, 1127 itemMinHeight: this.itemMinHeight, 1128 itemPadding: this.itemPadding, 1129 languageDirection: this.languageDirection, 1130 }) 1131 .borderRadius(this.getItemButtonBorderRadius(repeatItem)) 1132 .backgroundBlurStyle(this.getItemBackgroundBlurStyle(), this.getItemBackgroundBlurStyleOptions()) 1133 .direction(this.languageDirection) 1134 } 1135 .accessibilityGroup(true) 1136 .accessibilityChecked(this.isSelected(repeatItem)) 1137 .accessibilityText(this.getItemAccessibilityText(repeatItem)) 1138 .accessibilityDescription(this.getItemAccessibilityDescription(repeatItem)) 1139 .accessibilityLevel(repeatItem.item.accessibilityLevel) 1140 .backgroundColor(this.getItemBackgroundColor(repeatItem)) 1141 .backgroundEffect(this.itemBackgroundEffect) 1142 .borderRadius(this.getItemButtonBorderRadius(repeatItem)) 1143 .constraintSize({ minHeight: this.getItemMinHeight() }) 1144 .direction(this.languageDirection) 1145 .enabled(repeatItem.item.enabled) 1146 .focusScopePriority(this.focusGroupId, this.getFocusPriority(repeatItem)) 1147 .layoutWeight(1) 1148 .padding(0) 1149 .onClick(() => { 1150 this.onItemClicked?.(repeatItem.index); 1151 let selection: number[]; 1152 const items = this.getItems(); 1153 const selectedIndexes = this.selectedIndexes ?? []; 1154 if (this.isSelected(repeatItem)) { 1155 selection = selectedIndexes.filter((index) => { 1156 if (index < 0 || index > items.length - 1) { 1157 return false; 1158 } 1159 return index !== repeatItem.index; 1160 }); 1161 } else { 1162 selection = selectedIndexes 1163 .filter((index) => index >= 0 && index <= items.length - 1) 1164 .concat(repeatItem.index); 1165 } 1166 this.$selectedIndexes(selection); 1167 }) 1168 }) 1169 .key(generateUniqueKye(this.focusGroupId)) 1170 } 1171 .clip(false) 1172 .direction(this.languageDirection) 1173 .focusScopeId(this.focusGroupId, true) 1174 } 1175 1176 private getFocusPriority(repeatItem: RepeatItem<SegmentButtonV2Item>): FocusPriority | undefined { 1177 return Math.min(...this.selectedIndexes) === repeatItem.index ? FocusPriority.PREVIOUS : FocusPriority.AUTO; 1178 } 1179 1180 getItems(): SegmentButtonV2Items { 1181 return this.items ?? EMPTY_ITEMS; 1182 } 1183 1184 getItemBackgroundBlurStyleOptions(): BackgroundBlurStyleOptions | undefined { 1185 if (this.itemBackgroundEffect) { 1186 return undefined; 1187 } 1188 return this.itemBackgroundBlurStyleOptions; 1189 } 1190 1191 getItemBackgroundBlurStyle(): BlurStyle | undefined { 1192 if (this.itemBackgroundEffect) { 1193 return undefined; 1194 } 1195 return this.itemBackgroundBlurStyle; 1196 } 1197 1198 private getItemAccessibilityDescription(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 1199 return repeatItem.item.accessibilityDescription as ESObject as string; 1200 } 1201 1202 private getItemAccessibilityText(repeatItem: RepeatItem<SegmentButtonV2Item>): string | undefined { 1203 return repeatItem.item.accessibilityText as ESObject as string; 1204 } 1205 1206 private getItemSpace(): LengthMetrics { 1207 if (this.itemSpace && this.itemSpace.unit !== LengthUnit.PERCENT 1208 && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSpace)) { 1209 return this.itemSpace; 1210 } 1211 return this.theme.itemSpace; 1212 } 1213 1214 private getItemMinHeight(): Length | undefined { 1215 if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) { 1216 return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight); 1217 } 1218 return this.theme.itemMinHeight; 1219 } 1220 1221 private getItemBackgroundColor(repeatItem: RepeatItem<SegmentButtonV2Item>): ResourceColor { 1222 if (this.isSelected(repeatItem)) { 1223 return this.itemSelectedBackgroundColor?.color ?? this.theme.itemSelectedBackgroundColor; 1224 } 1225 return this.itemBackgroundColor?.color ?? this.theme.itemBackgroundColor; 1226 } 1227 1228 private isSelected(repeatItem: RepeatItem<SegmentButtonV2Item>): boolean { 1229 const selectedIndexes = this.selectedIndexes ?? []; 1230 return selectedIndexes.includes(repeatItem.index); 1231 } 1232 1233 private getItemButtonBorderRadius(repeatItem: RepeatItem<SegmentButtonV2Item>): LocalizedBorderRadiuses { 1234 const items = this.getItems(); 1235 const noneBorderRadius: LengthMetrics = LengthMetrics.vp(0); 1236 const borderRadiuses: LocalizedBorderRadiuses = { 1237 topStart: noneBorderRadius, 1238 bottomStart: noneBorderRadius, 1239 topEnd: noneBorderRadius, 1240 bottomEnd: noneBorderRadius, 1241 }; 1242 if (repeatItem.index === 0) { 1243 const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius); 1244 borderRadiuses.topStart = borderRadius; 1245 borderRadiuses.bottomStart = borderRadius; 1246 1247 } 1248 if (repeatItem.index === items.length - 1) { 1249 const borderRadius: LengthMetrics = this.itemBorderRadius ?? LengthMetrics.resource(this.theme.itemBorderRadius); 1250 borderRadiuses.topEnd = borderRadius; 1251 borderRadiuses.bottomEnd = borderRadius; 1252 } 1253 return borderRadiuses; 1254 } 1255} 1256 1257@ComponentV2 1258struct SegmentButtonV2ItemContent { 1259 @Require 1260 @Param 1261 hasHybrid: boolean; 1262 @Require 1263 @Param 1264 item: SegmentButtonV2Item; 1265 @Require 1266 @Param 1267 selected: boolean; 1268 @Require 1269 @Param 1270 theme: SegmentButtonV2ContentTheme; 1271 @Require 1272 @Param 1273 itemMinFontScale?: number | Resource = undefined; 1274 @Require 1275 @Param 1276 itemMaxFontScale?: number | Resource = undefined; 1277 @Require 1278 @Param 1279 itemFontColor?: ColorMetrics = undefined; 1280 @Require 1281 @Param 1282 itemSelectedFontColor?: ColorMetrics = undefined; 1283 @Require 1284 @Param 1285 itemFontSize?: LengthMetrics = undefined; 1286 @Require 1287 @Param 1288 itemSelectedFontSize?: LengthMetrics = undefined; 1289 @Require 1290 @Param 1291 itemFontWeight?: FontWeight = undefined; 1292 @Require 1293 @Param 1294 itemSelectedFontWeight?: FontWeight = undefined; 1295 @Require 1296 @Param 1297 itemIconSize?: SizeT<LengthMetrics> = undefined; 1298 @Require 1299 @Param 1300 itemIconFillColor?: ColorMetrics = undefined; 1301 @Require 1302 @Param 1303 itemSelectedIconFillColor?: ColorMetrics = undefined; 1304 @Require 1305 @Param 1306 itemSymbolFontSize?: LengthMetrics = undefined; 1307 @Require 1308 @Param 1309 itemSymbolFontColor?: ColorMetrics = undefined; 1310 @Require 1311 @Param 1312 itemSelectedSymbolFontColor?: ColorMetrics = undefined; 1313 @Require 1314 @Param 1315 itemMinHeight?: LengthMetrics = undefined; 1316 @Require 1317 @Param 1318 itemPadding?: LocalizedPadding = undefined; 1319 @Require 1320 @Param 1321 languageDirection?: Direction = undefined; 1322 1323 build() { 1324 Column({ space: 2 }) { 1325 if (this.item.symbol || this.item.symbolModifier) { 1326 SymbolGlyph(this.item.symbol) 1327 .fontSize(this.getSymbolFontSize()) 1328 .fontColor([this.getItemSymbolFillColor()]) 1329 .direction(this.languageDirection) 1330 .attributeModifier(this.item.symbolModifier) 1331 } else if (this.item.icon) { 1332 Image(this.item.icon) 1333 .fillColor(this.getItemIconFillColor()) 1334 .width(this.getItemIconWidth()) 1335 .height(this.getItemIconHeight()) 1336 .direction(this.languageDirection) 1337 .draggable(false) 1338 .attributeModifier(this.item.iconModifier) 1339 } 1340 1341 if (this.item.text) { 1342 Text(this.item.text) 1343 .direction(this.languageDirection) 1344 .fontSize(this.getItemFontSize()) 1345 .fontColor(this.getItemFontColor()) 1346 .fontWeight(this.getItemFontWeight()) 1347 .textOverflow({ overflow: TextOverflow.Ellipsis }) 1348 .maxLines(1) 1349 .maxFontScale(this.getItemMaxFontScale()) 1350 .minFontScale(this.getItemMinFontScale()) 1351 .attributeModifier(this.item.textModifier) 1352 } 1353 } 1354 .constraintSize({ minHeight: this.getItemMinHeight(), minWidth: '100%' }) 1355 .direction(this.languageDirection) 1356 .justifyContent(FlexAlign.Center) 1357 .padding(this.getItemPadding()) 1358 } 1359 1360 private getItemFontWeight(): string | number | FontWeight { 1361 if (this.selected) { 1362 return this.itemSelectedFontWeight ?? this.theme.itemSelectedFontWeight; 1363 } 1364 return this.itemFontWeight ?? this.theme.itemFontWeight; 1365 } 1366 1367 getItemSymbolFillColor(): ResourceColor { 1368 if (this.selected) { 1369 return this.itemSelectedSymbolFontColor?.color ?? this.theme.itemSelectedSymbolFontColor; 1370 } 1371 return this.itemSymbolFontColor?.color ?? this.theme.itemSymbolFontColor; 1372 } 1373 1374 private getSymbolFontSize(): Dimension { 1375 if (this.itemSymbolFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSymbolFontSize) && this.itemSymbolFontSize.unit !== LengthUnit.PERCENT) { 1376 return LengthMetricsUtils.getInstance().stringify(this.itemSymbolFontSize); 1377 } 1378 return this.theme.itemSymbolFontSize; 1379 } 1380 1381 private getItemMaxFontScale() { 1382 if (typeof this.itemMaxFontScale === 'number') { 1383 return normalize(this.itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest); 1384 } 1385 if (typeof this.itemMaxFontScale === 'object') { 1386 const itemMaxFontScale: number = 1387 parseNumericResource(this.getUIContext(), this.itemMaxFontScale) ?? SMALLEST_MAX_FONT_SCALE; 1388 return normalize(itemMaxFontScale, this.theme.itemMaxFontScaleSmallest, this.theme.itemMaxFontScaleLargest); 1389 } 1390 return SMALLEST_MAX_FONT_SCALE; 1391 } 1392 1393 private getItemMinFontScale() { 1394 if (typeof this.itemMinFontScale === 'number') { 1395 return normalize(this.itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest); 1396 } 1397 if (typeof this.itemMinFontScale === 'object') { 1398 const itemMinFontScale = 1399 parseNumericResource(this.getUIContext(), this.itemMinFontScale) ?? SMALLEST_MIN_FONT_SCALE; 1400 return normalize(itemMinFontScale, this.theme.itemMinFontScaleSmallest, this.theme.itemMinFontScaleLargest); 1401 } 1402 return SMALLEST_MIN_FONT_SCALE; 1403 } 1404 1405 private getItemPadding(): LocalizedPadding | Length | Padding { 1406 const itemPadding: LocalizedPadding = { 1407 top: this.theme.itemPadding.top, 1408 bottom: this.theme.itemPadding.bottom, 1409 start: this.theme.itemPadding.start, 1410 end: this.theme.itemPadding.end, 1411 }; 1412 1413 if (this.itemPadding?.top && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.top)) { 1414 itemPadding.top = this.itemPadding.top; 1415 } 1416 1417 if (this.itemPadding?.bottom && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.bottom)) { 1418 itemPadding.bottom = this.itemPadding.bottom; 1419 } 1420 1421 if (this.itemPadding?.start && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.start)) { 1422 itemPadding.start = this.itemPadding.start; 1423 } 1424 1425 if (this.itemPadding?.end && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemPadding.end)) { 1426 itemPadding.end = this.itemPadding.end; 1427 } 1428 1429 return itemPadding; 1430 } 1431 1432 private getItemMinHeight(): Length | undefined { 1433 if (this.itemMinHeight && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemMinHeight)) { 1434 return LengthMetricsUtils.getInstance().stringify(this.itemMinHeight); 1435 } 1436 return this.hasHybrid ? this.theme.hybridItemMinHeight : this.theme.itemMinHeight; 1437 } 1438 1439 private getItemFontColor(): ResourceColor { 1440 if (this.selected) { 1441 if (this.itemSelectedFontColor) { 1442 return this.itemSelectedFontColor.color; 1443 } 1444 return this.theme.itemSelectedFontColor; 1445 } 1446 if (this.itemFontColor) { 1447 return this.itemFontColor.color; 1448 } 1449 return this.theme.itemFontColor; 1450 } 1451 1452 private getItemFontSize(): Length { 1453 if (this.selected) { 1454 if (this.itemSelectedFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemSelectedFontSize) && this.itemSelectedFontSize.unit !== LengthUnit.PERCENT) { 1455 return LengthMetricsUtils.getInstance().stringify(this.itemSelectedFontSize); 1456 } 1457 return this.theme.itemFontSize; 1458 } 1459 if (this.itemFontSize && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemFontSize) && this.itemFontSize.unit !== LengthUnit.PERCENT) { 1460 return LengthMetricsUtils.getInstance().stringify(this.itemFontSize); 1461 } 1462 return this.theme.itemFontSize; 1463 } 1464 1465 private getItemIconHeight(): Length { 1466 if (this.itemIconSize?.height && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.height)) { 1467 return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.height); 1468 } 1469 return this.theme.itemIconSize; 1470 } 1471 1472 private getItemIconWidth(): Length { 1473 if (this.itemIconSize?.width && LengthMetricsUtils.getInstance().isNaturalNumber(this.itemIconSize.width)) { 1474 return LengthMetricsUtils.getInstance().stringify(this.itemIconSize.width); 1475 } 1476 return this.theme.itemIconSize; 1477 } 1478 1479 private getItemIconFillColor(): ResourceColor { 1480 if (this.selected) { 1481 if (this.itemSelectedIconFillColor) { 1482 return this.itemSelectedIconFillColor.color; 1483 } 1484 return this.theme.itemSelectedIconFillColor; 1485 } 1486 if (this.itemIconFillColor) { 1487 return this.itemIconFillColor.color; 1488 } 1489 return this.theme.itemIconFillColor; 1490 } 1491} 1492 1493class LengthMetricsUtils { 1494 private static instance?: LengthMetricsUtils; 1495 1496 private constructor() { 1497 } 1498 1499 public static getInstance(): LengthMetricsUtils { 1500 if (!LengthMetricsUtils.instance) { 1501 LengthMetricsUtils.instance = new LengthMetricsUtils(); 1502 } 1503 return LengthMetricsUtils.instance; 1504 } 1505 1506 stringify(metrics: LengthMetrics): Dimension { 1507 switch (metrics.unit) { 1508 case LengthUnit.PX: 1509 return `${metrics.value}px`; 1510 case LengthUnit.VP: 1511 return `${metrics.value}vp`; 1512 case LengthUnit.FP: 1513 return `${metrics.value}fp`; 1514 case LengthUnit.PERCENT: 1515 return `${metrics.value}%`; 1516 case LengthUnit.LPX: 1517 return `${metrics.value}lpx`; 1518 } 1519 } 1520 1521 isNaturalNumber(metrics: LengthMetrics): boolean { 1522 return metrics.value >= 0; 1523 } 1524} 1525 1526function parseNumericResource(context: UIContext, resource: Resource): number | undefined { 1527 const resourceManager = context.getHostContext()?.resourceManager; 1528 if (!resourceManager) { 1529 return undefined; 1530 } 1531 try { 1532 return resourceManager.getNumber(resource); 1533 } catch (err) { 1534 // todo log err 1535 return undefined; 1536 } 1537} 1538 1539function normalize(value: number, min: number, max: number): number { 1540 return Math.min(Math.max(value, min), max); 1541} 1542 1543function generateUniqueKye(groupId: string) { 1544 return (item: SegmentButtonV2Item, index: number): string => { 1545 let key = groupId; 1546 if (item.text) { 1547 if (typeof item.text === 'string') { 1548 key += item.text; 1549 } else { 1550 key += getResourceUniqueId(item.text); 1551 } 1552 } 1553 if (item.icon) { 1554 if (typeof item.icon === 'string') { 1555 key += item.icon; 1556 } else { 1557 key += getResourceUniqueId(item.icon); 1558 } 1559 } 1560 if (item.symbol) { 1561 key += getResourceUniqueId(item.symbol); 1562 } 1563 return key; 1564 } 1565} 1566 1567function getResourceUniqueId(resource: Resource): string { 1568 if (resource.id !== -1) { 1569 return `${resource.id}`; 1570 } else { 1571 return JSON.stringify(resource); 1572 } 1573} 1574