1/* 2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15import curves from '@ohos.curves'; 16import { KeyCode } from '@ohos.multimodalInput.keyCode'; 17import util from '@ohos.util'; 18import { LengthMetrics, LengthUnit } from '@ohos.arkui.node'; 19import I18n from '@ohos.i18n'; 20 21const MIN_ITEM_COUNT = 2 22const MAX_ITEM_COUNT = 5 23const DEFAULT_MAX_FONT_SCALE: number = 1 24const MAX_MAX_FONT_SCALE: number = 2 25const MIN_MAX_FONT_SCALE: number = 1 26const RESOURCE_TYPE_FLOAT = 10002; 27const RESOURCE_TYPE_INTEGER = 10007; 28const CAPSULE_FOCUS_SELECTED_OFFSET: number = 4; 29// Space character for selected accessibility description - prevents screen readers from announcing 30const ACCESSIBILITY_SELECTED_DESCRIPTION = ' '; 31const ACCESSIBILITY_DEFAULT_DESCRIPTION = ''; 32 33interface SegmentButtonThemeInterface { 34 SEGMENT_TEXT_VERTICAL_PADDING: Resource; 35 SEGMENT_TEXT_HORIZONTAL_PADDING: Resource; 36 SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING: Resource; 37 SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR: ResourceColor; 38 FONT_COLOR: ResourceColor, 39 TAB_SELECTED_FONT_COLOR: ResourceColor, 40 CAPSULE_SELECTED_FONT_COLOR: ResourceColor, 41 FONT_SIZE: DimensionNoPercentage, 42 SELECTED_FONT_SIZE: DimensionNoPercentage, 43 BACKGROUND_COLOR: ResourceColor, 44 TAB_SELECTED_BACKGROUND_COLOR: ResourceColor, 45 CAPSULE_SELECTED_BACKGROUND_COLOR: ResourceColor, 46 FOCUS_BORDER_COLOR: ResourceColor, 47 HOVER_COLOR: ResourceColor, 48 PRESS_COLOR: ResourceColor, 49 BACKGROUND_BLUR_STYLE: Resource, 50 CONSTRAINT_SIZE_MIN_HEIGHT: DimensionNoPercentage, 51 SEGMENT_BUTTON_MIN_FONT_SIZE: DimensionNoPercentage, 52 SEGMENT_BUTTON_NORMAL_BORDER_RADIUS: Length | BorderRadiuses | LocalizedBorderRadiuses, 53 SEGMENT_ITEM_TEXT_OVERFLOW: Resource, 54 SEGMENT_BUTTON_FOCUS_TEXT_COLOR: ResourceColor, 55 SEGMENT_BUTTON_SHADOW: Resource, 56 SEGMENT_FOCUS_STYLE_CUSTOMIZED: Resource, 57 SEGMENT_BUTTON_CONTAINER_SHAPE: Resource, 58 SEGMENT_BUTTON_SELECTED_BACKGROUND_SHAPE: Resource 59} 60 61const segmentButtonTheme: SegmentButtonThemeInterface = { 62 FONT_COLOR: $r('sys.color.segment_button_unselected_text_color'), 63 TAB_SELECTED_FONT_COLOR: $r('sys.color.segment_button_checked_text_color'), 64 CAPSULE_SELECTED_FONT_COLOR: $r('sys.color.ohos_id_color_foreground_contrary'), 65 FONT_SIZE: $r('sys.float.segment_button_unselected_text_size'), 66 SELECTED_FONT_SIZE: $r('sys.float.segment_button_checked_text_size'), 67 BACKGROUND_COLOR: $r('sys.color.segment_button_backboard_color'), 68 TAB_SELECTED_BACKGROUND_COLOR: $r('sys.color.segment_button_checked_foreground_color'), 69 CAPSULE_SELECTED_BACKGROUND_COLOR: $r('sys.color.ohos_id_color_emphasize'), 70 FOCUS_BORDER_COLOR: $r('sys.color.ohos_id_color_focused_outline'), 71 HOVER_COLOR: $r('sys.color.segment_button_hover_color'), 72 PRESS_COLOR: $r('sys.color.segment_button_press_color'), 73 BACKGROUND_BLUR_STYLE: $r('sys.float.segment_button_background_blur_style'), 74 CONSTRAINT_SIZE_MIN_HEIGHT: $r('sys.float.segment_button_height'), 75 SEGMENT_BUTTON_MIN_FONT_SIZE: $r('sys.float.segment_button_min_font_size'), 76 SEGMENT_BUTTON_NORMAL_BORDER_RADIUS: $r('sys.float.segment_button_normal_border_radius'), 77 SEGMENT_ITEM_TEXT_OVERFLOW: $r('sys.float.segment_marquee'), 78 SEGMENT_BUTTON_FOCUS_TEXT_COLOR: $r('sys.color.segment_button_focus_text_primary'), 79 SEGMENT_BUTTON_SHADOW: $r('sys.float.segment_button_shadow'), 80 SEGMENT_TEXT_HORIZONTAL_PADDING: $r('sys.float.segment_button_text_l_r_padding'), 81 SEGMENT_TEXT_VERTICAL_PADDING: $r('sys.float.segment_button_text_u_d_padding'), 82 SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING: $r('sys.float.segment_button_text_capsule_u_d_padding'), 83 SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR: $r('sys.color.segment_button_focus_backboard_primary'), 84 SEGMENT_FOCUS_STYLE_CUSTOMIZED: $r('sys.float.segment_focus_control'), 85 SEGMENT_BUTTON_CONTAINER_SHAPE: $r('sys.float.segmentbutton_container_shape'), 86 SEGMENT_BUTTON_SELECTED_BACKGROUND_SHAPE: $r('sys.float.segmentbutton_selected_background_shape') 87} 88 89interface Point { 90 x: number 91 y: number 92} 93 94function nearEqual(first: number, second: number): boolean { 95 return Math.abs(first - second) < 0.001 96} 97 98function validateLengthMetrics(value: LengthMetrics | undefined, defaultValue: LengthMetrics): LengthMetrics { 99 const actualValue = value ?? defaultValue; 100 return (actualValue.value < 0 || actualValue.unit === LengthUnit.PERCENT) ? defaultValue : actualValue; 101} 102 103interface SegmentButtonTextItem { 104 text: ResourceStr 105 accessibilityLevel?: string 106 accessibilityDescription?: ResourceStr 107} 108 109interface SegmentButtonIconItem { 110 icon: ResourceStr, 111 iconAccessibilityText?: ResourceStr 112 selectedIcon: ResourceStr 113 selectedIconAccessibilityText?: ResourceStr 114 accessibilityLevel?: string 115 accessibilityDescription?: ResourceStr 116} 117 118interface SegmentButtonIconTextItem { 119 icon: ResourceStr, 120 iconAccessibilityText?: ResourceStr 121 selectedIcon: ResourceStr, 122 selectedIconAccessibilityText?: ResourceStr 123 text: ResourceStr 124 accessibilityLevel?: string 125 accessibilityDescription?: ResourceStr 126} 127 128type DimensionNoPercentage = PX | VP | FP | LPX | Resource 129 130interface CommonSegmentButtonOptions { 131 fontColor?: ResourceColor 132 selectedFontColor?: ResourceColor 133 fontSize?: DimensionNoPercentage 134 selectedFontSize?: DimensionNoPercentage 135 fontWeight?: FontWeight 136 selectedFontWeight?: FontWeight 137 backgroundColor?: ResourceColor 138 selectedBackgroundColor?: ResourceColor 139 imageSize?: SizeOptions 140 buttonPadding?: Padding | Dimension 141 textPadding?: Padding | Dimension 142 localizedTextPadding?: LocalizedPadding 143 localizedButtonPadding?: LocalizedPadding 144 backgroundBlurStyle?: BlurStyle 145 direction?: Direction 146 borderRadiusMode?: BorderRadiusMode 147 backgroundBorderRadius?: LengthMetrics 148 itemBorderRadius?: LengthMetrics 149} 150 151type ItemRestriction<T> = [T, T, T?, T?, T?] 152type SegmentButtonItemTuple = ItemRestriction<SegmentButtonTextItem> | 153ItemRestriction<SegmentButtonIconItem> | ItemRestriction<SegmentButtonIconTextItem> 154type SegmentButtonItemArray = Array<SegmentButtonTextItem> | 155Array<SegmentButtonIconItem> | Array<SegmentButtonIconTextItem> 156 157export interface TabSegmentButtonConstructionOptions extends CommonSegmentButtonOptions { 158 buttons: ItemRestriction<SegmentButtonTextItem> 159} 160 161export interface CapsuleSegmentButtonConstructionOptions extends CommonSegmentButtonOptions { 162 buttons: SegmentButtonItemTuple 163 multiply?: boolean 164} 165 166export interface TabSegmentButtonOptions extends TabSegmentButtonConstructionOptions { 167 type: 'tab', 168} 169 170export interface CapsuleSegmentButtonOptions extends CapsuleSegmentButtonConstructionOptions { 171 type: 'capsule' 172} 173 174export enum BorderRadiusMode { 175 /** 176 * DEFAULT Mode, the framework automatically calculates the border radius 177 */ 178 DEFAULT = 0, 179 180 /** 181 * CUSTOM Mode, the developer sets the border radius 182 */ 183 CUSTOM = 1 184} 185 186interface SegmentButtonItemOptionsConstructorOptions { 187 icon?: ResourceStr 188 iconAccessibilityText?: ResourceStr 189 selectedIcon?: ResourceStr 190 selectedIconAccessibilityText?: ResourceStr 191 text?: ResourceStr 192 accessibilityLevel?: string 193 accessibilityDescription?: ResourceStr 194} 195 196@Observed 197class SegmentButtonItemOptions { 198 public icon?: ResourceStr 199 public iconAccessibilityText?: ResourceStr 200 public selectedIcon?: ResourceStr 201 public selectedIconAccessibilityText?: ResourceStr 202 public text?: ResourceStr 203 public accessibilityLevel?: string 204 public accessibilityDescription?: ResourceStr 205 206 constructor(options: SegmentButtonItemOptionsConstructorOptions) { 207 this.icon = options.icon 208 this.selectedIcon = options.selectedIcon 209 this.text = options.text 210 this.iconAccessibilityText = options.iconAccessibilityText 211 this.selectedIconAccessibilityText = options.selectedIconAccessibilityText 212 this.accessibilityLevel = options.accessibilityLevel 213 this.accessibilityDescription = options.accessibilityDescription 214 } 215} 216 217@Observed 218export class SegmentButtonItemOptionsArray extends Array<SegmentButtonItemOptions> { 219 public changeStartIndex: number | undefined = void 0 220 public deleteCount: number | undefined = void 0 221 public addLength: number | undefined = void 0 222 223 constructor(length: number) 224 225 constructor(elements: SegmentButtonItemTuple) 226 227 constructor(elementsOrLength: SegmentButtonItemTuple | number) { 228 229 super(typeof elementsOrLength === 'number' ? elementsOrLength : 0); 230 231 if (typeof elementsOrLength !== 'number' && elementsOrLength !== void 0) { 232 super.push(...elementsOrLength.map((element?: SegmentButtonTextItem | SegmentButtonIconItem | 233 SegmentButtonIconTextItem) => new SegmentButtonItemOptions(element as 234 SegmentButtonItemOptionsConstructorOptions))) 235 } 236 } 237 238 push(...items: SegmentButtonItemArray): number { 239 if (this.length + items.length > MAX_ITEM_COUNT) { 240 console.warn('Exceeded the maximum number of elements (5).') 241 return this.length 242 } 243 this.changeStartIndex = this.length 244 this.deleteCount = 0 245 this.addLength = items.length 246 return super.push(...items.map((element: SegmentButtonItemOptionsConstructorOptions) => 247 new SegmentButtonItemOptions(element))) 248 } 249 250 pop() { 251 if (this.length <= MIN_ITEM_COUNT) { 252 console.warn('Below the minimum number of elements (2).') 253 return void 0 254 } 255 this.changeStartIndex = this.length - 1 256 this.deleteCount = 1 257 this.addLength = 0 258 return super.pop() 259 } 260 261 shift() { 262 if (this.length <= MIN_ITEM_COUNT) { 263 console.warn('Below the minimum number of elements (2).') 264 return void 0 265 } 266 this.changeStartIndex = 0 267 this.deleteCount = 1 268 this.addLength = 0 269 return super.shift() 270 } 271 272 unshift(...items: SegmentButtonItemArray): number { 273 if (this.length + items.length > MAX_ITEM_COUNT) { 274 console.warn('Exceeded the maximum number of elements (5).') 275 return this.length 276 } 277 if (items.length > 0) { 278 this.changeStartIndex = 0 279 this.deleteCount = 0 280 this.addLength = items.length 281 } 282 return super.unshift(...items.map((element: SegmentButtonItemOptionsConstructorOptions) => 283 new SegmentButtonItemOptions(element))) 284 } 285 286 splice(start: number, deleteCount: number, ...items: SegmentButtonItemOptions[]): SegmentButtonItemOptions[] { 287 let length = (this.length - deleteCount) < 0 ? 0 : (this.length - deleteCount) 288 length += items.length 289 if (length < MIN_ITEM_COUNT) { 290 console.warn('Below the minimum number of elements (2).') 291 return [] 292 } 293 if (length > MAX_ITEM_COUNT) { 294 console.warn('Exceeded the maximum number of elements (5).') 295 return [] 296 } 297 this.changeStartIndex = start 298 this.deleteCount = deleteCount 299 this.addLength = items.length 300 return super.splice(start, deleteCount, ...items) 301 } 302 303 static create(elements: SegmentButtonItemTuple): SegmentButtonItemOptionsArray { 304 return new SegmentButtonItemOptionsArray(elements) 305 } 306} 307 308@Observed 309export class SegmentButtonOptions { 310 public type: 'tab' | 'capsule' 311 public multiply: boolean = false 312 public fontColor: ResourceColor 313 public selectedFontColor: ResourceColor 314 public fontSize: DimensionNoPercentage 315 public selectedFontSize: DimensionNoPercentage 316 public fontWeight: FontWeight 317 public selectedFontWeight: FontWeight 318 public backgroundColor: ResourceColor 319 public selectedBackgroundColor: ResourceColor 320 public imageSize: SizeOptions 321 public buttonPadding: Padding | Dimension | undefined 322 public textPadding: Padding | Dimension | undefined 323 public componentPadding: Padding | Dimension 324 public localizedTextPadding?: LocalizedPadding 325 public localizedButtonPadding?: LocalizedPadding 326 public showText: boolean = false 327 public showIcon: boolean = false 328 public iconTextRadius?: number 329 public iconTextBackgroundRadius?: number 330 public backgroundBlurStyle: BlurStyle 331 public direction?: Direction 332 public borderRadiusMode?: BorderRadiusMode 333 public backgroundBorderRadius?: LengthMetrics 334 public itemBorderRadius?: LengthMetrics 335 private _buttons: SegmentButtonItemOptionsArray | undefined = void 0 336 337 get buttons() { 338 return this._buttons 339 } 340 341 set buttons(val) { 342 if (this._buttons !== void 0 && this._buttons !== val) { 343 this.onButtonsChange?.() 344 } 345 this._buttons = val 346 } 347 348 public onButtonsChange?: () => void 349 350 constructor(options: TabSegmentButtonOptions | CapsuleSegmentButtonOptions) { 351 this.fontColor = options.fontColor ?? segmentButtonTheme.FONT_COLOR 352 this.selectedFontColor = options.selectedFontColor ?? segmentButtonTheme.TAB_SELECTED_FONT_COLOR 353 this.fontSize = options.fontSize ?? segmentButtonTheme.FONT_SIZE 354 this.selectedFontSize = options.selectedFontSize ?? segmentButtonTheme.SELECTED_FONT_SIZE 355 this.fontWeight = options.fontWeight ?? FontWeight.Regular 356 this.selectedFontWeight = options.selectedFontWeight ?? FontWeight.Medium 357 this.backgroundColor = options.backgroundColor ?? segmentButtonTheme.BACKGROUND_COLOR 358 this.selectedBackgroundColor = options.selectedBackgroundColor ?? segmentButtonTheme.TAB_SELECTED_BACKGROUND_COLOR 359 this.imageSize = options.imageSize ?? { width: 24, height: 24 } 360 this.buttonPadding = options.buttonPadding 361 this.textPadding = options.textPadding 362 this.type = options.type 363 this.backgroundBlurStyle = 364 options.backgroundBlurStyle ?? 365 LengthMetrics.resource(segmentButtonTheme.BACKGROUND_BLUR_STYLE).value as BlurStyle; 366 this.localizedTextPadding = options.localizedTextPadding 367 this.localizedButtonPadding = options.localizedButtonPadding 368 this.direction = options.direction ?? Direction.Auto 369 this.borderRadiusMode = options.borderRadiusMode ?? BorderRadiusMode.DEFAULT 370 if (this.borderRadiusMode !== BorderRadiusMode.DEFAULT && 371 this.borderRadiusMode !== BorderRadiusMode.CUSTOM) { 372 this.borderRadiusMode = BorderRadiusMode.DEFAULT; 373 } 374 this.backgroundBorderRadius = validateLengthMetrics( 375 options.backgroundBorderRadius, 376 LengthMetrics.resource(segmentButtonTheme.SEGMENT_BUTTON_CONTAINER_SHAPE) 377 ); 378 this.itemBorderRadius = validateLengthMetrics( 379 options.itemBorderRadius, 380 LengthMetrics.resource(segmentButtonTheme.SEGMENT_BUTTON_SELECTED_BACKGROUND_SHAPE) 381 ); 382 this.buttons = new SegmentButtonItemOptionsArray(options.buttons) 383 if (this.type === 'capsule') { 384 this.multiply = (options as CapsuleSegmentButtonOptions).multiply ?? false 385 this.onButtonsUpdated(); 386 this.selectedFontColor = options.selectedFontColor ?? segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR 387 this.selectedBackgroundColor = options.selectedBackgroundColor ?? 388 segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR 389 } else { 390 this.showText = true 391 } 392 let themePadding = LengthMetrics.resource($r('sys.float.segment_button_baseplate_padding')).value; 393 this.componentPadding = this.multiply ? 0 : themePadding; 394 } 395 396 public onButtonsUpdated() { 397 this.buttons?.forEach(button => { 398 this.showText ||= button.text !== void 0; 399 this.showIcon ||= button.icon !== void 0 || button.selectedIcon !== void 0; 400 }) 401 if (this.showText && this.showIcon) { 402 this.iconTextRadius = 12; 403 this.iconTextBackgroundRadius = 14; 404 } 405 } 406 407 static tab(options: TabSegmentButtonConstructionOptions): SegmentButtonOptions { 408 return new SegmentButtonOptions({ 409 type: 'tab', 410 buttons: options.buttons, 411 fontColor: options.fontColor, 412 selectedFontColor: options.selectedFontColor, 413 fontSize: options.fontSize, 414 selectedFontSize: options.selectedFontSize, 415 fontWeight: options.fontWeight, 416 selectedFontWeight: options.selectedFontWeight, 417 backgroundColor: options.backgroundColor, 418 selectedBackgroundColor: options.selectedBackgroundColor, 419 imageSize: options.imageSize, 420 buttonPadding: options.buttonPadding, 421 textPadding: options.textPadding, 422 localizedTextPadding: options.localizedTextPadding, 423 localizedButtonPadding: options.localizedButtonPadding, 424 backgroundBlurStyle: options.backgroundBlurStyle, 425 direction: options.direction, 426 borderRadiusMode: options.borderRadiusMode, 427 backgroundBorderRadius: options.backgroundBorderRadius, 428 itemBorderRadius: options.itemBorderRadius 429 }) 430 } 431 432 static capsule(options: CapsuleSegmentButtonConstructionOptions): SegmentButtonOptions { 433 return new SegmentButtonOptions({ 434 type: 'capsule', 435 buttons: options.buttons, 436 multiply: options.multiply, 437 fontColor: options.fontColor, 438 selectedFontColor: options.selectedFontColor, 439 fontSize: options.fontSize, 440 selectedFontSize: options.selectedFontSize, 441 fontWeight: options.fontWeight, 442 selectedFontWeight: options.selectedFontWeight, 443 backgroundColor: options.backgroundColor, 444 selectedBackgroundColor: options.selectedBackgroundColor, 445 imageSize: options.imageSize, 446 buttonPadding: options.buttonPadding, 447 textPadding: options.textPadding, 448 localizedTextPadding: options.localizedTextPadding, 449 localizedButtonPadding: options.localizedButtonPadding, 450 backgroundBlurStyle: options.backgroundBlurStyle, 451 direction: options.direction, 452 borderRadiusMode: options.borderRadiusMode, 453 backgroundBorderRadius: options.backgroundBorderRadius, 454 itemBorderRadius: options.itemBorderRadius 455 }) 456 } 457} 458 459@Component 460struct MultiSelectBackground { 461 @ObjectLink optionsArray: SegmentButtonItemOptionsArray 462 @ObjectLink options: SegmentButtonOptions 463 @Consume buttonBorderRadius: LocalizedBorderRadiuses[] 464 @Consume buttonItemsSize: SizeOptions[] 465 466 build() { 467 Row({ space: 1 }) { 468 ForEach(this.optionsArray, (_: SegmentButtonItemOptions, index) => { 469 if (index < MAX_ITEM_COUNT) { 470 Stack() 471 .direction(this.options.direction) 472 .layoutWeight(1) 473 .height(this.buttonItemsSize[index].height) 474 .backgroundColor(this.options.backgroundColor ?? segmentButtonTheme.BACKGROUND_COLOR) 475 .borderRadius(this.buttonBorderRadius[index]) 476 .backgroundBlurStyle(this.options.backgroundBlurStyle, undefined, { disableSystemAdaptation: true }) 477 } 478 }) 479 } 480 .direction(this.options.direction) 481 .padding(this.options.componentPadding) 482 } 483} 484 485@Component 486struct SelectItem { 487 @ObjectLink optionsArray: SegmentButtonItemOptionsArray 488 @ObjectLink options: SegmentButtonOptions 489 @Link selectedIndexes: number[] 490 @Consume buttonItemsSize: SizeOptions[] 491 @Consume selectedItemPosition: LocalizedEdges 492 @Consume zoomScaleArray: number[] 493 @Consume buttonBorderRadius: LocalizedBorderRadiuses[] 494 495 build() { 496 if (this.selectedIndexes !== void 0 && this.selectedIndexes.length !== 0) { 497 Stack() 498 .direction(this.options.direction) 499 .borderRadius(this.buttonBorderRadius[this.selectedIndexes[0]]) 500 .size(this.buttonItemsSize[this.selectedIndexes[0]]) 501 .backgroundColor(this.options.selectedBackgroundColor ?? 502 (this.options.type === 'tab' ? segmentButtonTheme.TAB_SELECTED_BACKGROUND_COLOR : 503 segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR)) 504 .position(this.selectedItemPosition) 505 .scale({ x: this.zoomScaleArray[this.selectedIndexes[0]], y: this.zoomScaleArray[this.selectedIndexes[0]] }) 506 .shadow(resourceToNumber(this.getUIContext()?.getHostContext(), segmentButtonTheme.SEGMENT_BUTTON_SHADOW, 507 0) as ShadowStyle) 508 } 509 } 510} 511 512@Component 513struct MultiSelectItemArray { 514 @ObjectLink optionsArray: SegmentButtonItemOptionsArray 515 @ObjectLink @Watch('onOptionsChange') options: SegmentButtonOptions 516 @Link @Watch('onSelectedChange') selectedIndexes: number[] 517 @Consume buttonItemsSize: SizeOptions[] 518 @Consume zoomScaleArray: number[] 519 @Consume buttonBorderRadius: LocalizedBorderRadiuses[] 520 @State multiColor: ResourceColor[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => Color.Transparent) 521 522 onOptionsChange() { 523 for (let i = 0; i < this.selectedIndexes.length; i++) { 524 this.multiColor[this.selectedIndexes[i]] = this.options.selectedBackgroundColor ?? 525 segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR 526 } 527 } 528 529 onSelectedChange() { 530 for (let i = 0; i < MAX_ITEM_COUNT; i++) { 531 this.multiColor[i] = Color.Transparent 532 } 533 for (let i = 0; i < this.selectedIndexes.length; i++) { 534 this.multiColor[this.selectedIndexes[i]] = this.options.selectedBackgroundColor ?? 535 segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR 536 } 537 } 538 539 aboutToAppear() { 540 for (let i = 0; i < this.selectedIndexes.length; i++) { 541 this.multiColor[this.selectedIndexes[i]] = this.options.selectedBackgroundColor ?? 542 segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR 543 } 544 } 545 546 build() { 547 Row({ space: 1 }) { 548 ForEach(this.optionsArray, (_: SegmentButtonItemOptions, index) => { 549 if (index < MAX_ITEM_COUNT) { 550 Stack() 551 .direction(this.options.direction) 552 .width(this.buttonItemsSize[index].width) 553 .height(this.buttonItemsSize[index].height) 554 .backgroundColor(this.multiColor[index]) 555 .borderRadius(this.buttonBorderRadius[index]) 556 } 557 }) 558 } 559 .direction(this.options.direction) 560 .padding(this.options.componentPadding) 561 } 562} 563 564@Component 565struct SegmentButtonItem { 566 @Link selectedIndexes: number[] 567 @Link @Watch('onFocusIndex') focusIndex: number; 568 @Prop @Require maxFontScale: number | Resource 569 @ObjectLink itemOptions: SegmentButtonItemOptions 570 @ObjectLink options: SegmentButtonOptions; 571 @ObjectLink property: ItemProperty 572 @Prop index: number 573 @State isTextSupportMarquee: boolean = 574 resourceToNumber(this.getUIContext()?.getHostContext(), segmentButtonTheme.SEGMENT_ITEM_TEXT_OVERFLOW, 1.0) === 0.0; 575 @Prop isMarqueeAndFadeout: boolean; 576 @Prop isSegmentFocusStyleCustomized: boolean; 577 @State isTextInMarqueeCondition: boolean = false; 578 @State isButtonTextFadeout?: boolean = false; 579 private groupId: string = '' 580 @Prop @Watch('onFocusIndex') hover: boolean; 581 582 private getTextPadding(): Padding | Dimension | LocalizedPadding { 583 if (this.options.localizedTextPadding) { 584 return this.options.localizedTextPadding 585 } 586 if (this.options.textPadding !== void (0)) { 587 return this.options.textPadding 588 } 589 return 0 590 } 591 592 private getButtonPadding(): Padding | Dimension | LocalizedPadding { 593 if (this.options.localizedButtonPadding) { 594 return this.options.localizedButtonPadding 595 } 596 if (this.options.buttonPadding !== void (0)) { 597 return this.options.buttonPadding 598 } 599 if (this.options.type === 'capsule' && this.options.showText && this.options.showIcon) { 600 return { 601 top: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING), 602 bottom: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_CAPSULE_VERTICAL_PADDING), 603 start: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING), 604 end: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING) 605 } 606 } 607 return { 608 top: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_VERTICAL_PADDING), 609 bottom: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_VERTICAL_PADDING), 610 start: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING), 611 end: LengthMetrics.resource(segmentButtonTheme.SEGMENT_TEXT_HORIZONTAL_PADDING) 612 } 613 } 614 615 onFocusIndex(): void { 616 this.isTextInMarqueeCondition = 617 this.isSegmentFocusStyleCustomized && (this.focusIndex === this.index || this.hover); 618 } 619 620 aboutToAppear(): void { 621 this.isButtonTextFadeout = this.isSegmentFocusStyleCustomized; 622 } 623 624 isDefaultSelectedFontColor(): boolean { 625 if (this.options.type === 'tab') { 626 return this.options.selectedFontColor === segmentButtonTheme.TAB_SELECTED_FONT_COLOR; 627 } else if (this.options.type === 'capsule') { 628 return this.options.selectedFontColor === segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR; 629 } 630 return false; 631 } 632 633 private getFontColor(): ResourceColor { 634 if (this.property.isSelected) { 635 if (this.isDefaultSelectedFontColor() && this.isSegmentFocusStyleCustomized && this.focusIndex === this.index) { 636 return segmentButtonTheme.SEGMENT_BUTTON_FOCUS_TEXT_COLOR; 637 } 638 return this.options.selectedFontColor ?? segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR; 639 } 640 return this.options.fontColor ?? segmentButtonTheme.FONT_COLOR; 641 } 642 643 private getAccessibilityText(): Resource | undefined { 644 if (this.selectedIndexes.includes(this.index) && 645 typeof this.itemOptions.selectedIconAccessibilityText !== undefined) { 646 return this.itemOptions.selectedIconAccessibilityText as Resource 647 } else if (!this.selectedIndexes.includes(this.index) && 648 typeof this.itemOptions.iconAccessibilityText !== undefined) { 649 return this.itemOptions.iconAccessibilityText as Resource 650 } 651 return undefined; 652 } 653 654 build() { 655 Column({ space: 2 }) { 656 if (this.options.showIcon) { 657 Image(this.property.isSelected ? this.itemOptions.selectedIcon : this.itemOptions.icon) 658 .direction(this.options.direction) 659 .size(this.options.imageSize ?? { width: 24, height: 24 }) 660 .draggable(false) 661 .fillColor(this.getFontColor()) 662 .accessibilityText(this.getAccessibilityText()) 663 } 664 if (this.options.showText) { 665 Text(this.itemOptions.text) 666 .direction(this.options.direction) 667 .fontColor(this.getFontColor()) 668 .fontWeight(this.property.fontWeight) 669 .fontSize(this.property.fontSize) 670 .minFontSize(this.isSegmentFocusStyleCustomized ? this.property.fontSize : 9) 671 .maxFontSize(this.property.fontSize) 672 .maxFontScale(this.maxFontScale) 673 .textOverflow({ 674 overflow: this.isTextSupportMarquee ? TextOverflow.MARQUEE : TextOverflow.Ellipsis 675 }) 676 .marqueeOptions({ 677 start: this.isTextInMarqueeCondition, 678 fadeout: this.isButtonTextFadeout, 679 marqueeStartPolicy: MarqueeStartPolicy.DEFAULT 680 }) 681 .maxLines(1) 682 .textAlign(TextAlign.Center) 683 .padding(this.getTextPadding()) 684 } 685 } 686 .direction(this.options.direction) 687 .justifyContent(FlexAlign.Center) 688 .padding(this.getButtonPadding()) 689 .constraintSize({ minHeight: segmentButtonTheme.CONSTRAINT_SIZE_MIN_HEIGHT }) 690 } 691} 692 693@Observed 694class HoverColorProperty { 695 public hoverColor: ResourceColor = Color.Transparent 696} 697 698@Component 699struct PressAndHoverEffect { 700 @Consume buttonItemsSize: SizeOptions[] 701 @Prop press: boolean 702 @Prop hover: boolean 703 @ObjectLink colorProperty: HoverColorProperty 704 @Consume buttonBorderRadius: LocalizedBorderRadiuses[] 705 @ObjectLink options: SegmentButtonOptions; 706 pressIndex: number = 0 707 pressColor: ResourceColor = segmentButtonTheme.PRESS_COLOR 708 709 build() { 710 Stack() 711 .direction(this.options.direction) 712 .size(this.buttonItemsSize[this.pressIndex]) 713 .backgroundColor(this.press && this.hover ? this.pressColor : this.colorProperty.hoverColor) 714 .borderRadius(this.buttonBorderRadius[this.pressIndex]) 715 } 716} 717 718@Component 719struct PressAndHoverEffectArray { 720 @ObjectLink buttons: SegmentButtonItemOptionsArray 721 @ObjectLink options: SegmentButtonOptions 722 @Link pressArray: boolean[] 723 @Link hoverArray: boolean[] 724 @Link hoverColorArray: HoverColorProperty[] 725 @Consume zoomScaleArray: number[] 726 727 build() { 728 Row({ space: 1 }) { 729 ForEach(this.buttons, (item: SegmentButtonItemOptions, index) => { 730 if (index < MAX_ITEM_COUNT) { 731 Stack() { 732 PressAndHoverEffect({ 733 pressIndex: index, 734 colorProperty: this.hoverColorArray[index], 735 press: this.pressArray[index], 736 hover: this.hoverArray[index], 737 options: this.options, 738 }) 739 } 740 .direction(this.options.direction) 741 .scale({ 742 x: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index], 743 y: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index] 744 }) 745 } 746 }) 747 }.direction(this.options.direction) 748 } 749} 750 751@Component 752struct SegmentButtonItemArrayComponent { 753 @ObjectLink @Watch('onOptionsArrayChange') optionsArray: SegmentButtonItemOptionsArray 754 @ObjectLink @Watch('onOptionsChange') options: SegmentButtonOptions 755 @Link selectedIndexes: number[] 756 @Consume componentSize: SizeOptions 757 @Consume buttonBorderRadius: LocalizedBorderRadiuses[] 758 @Consume @Watch('onButtonItemsSizeChange') buttonItemsSize: SizeOptions[] 759 @Consume buttonItemsPosition: LocalizedEdges[] 760 @Consume @Watch('onFocusIndex') focusIndex: number; 761 @Consume zoomScaleArray: number[] 762 @Consume buttonItemProperty: ItemProperty[] 763 @Consume buttonItemsSelected: boolean[] 764 @Link pressArray: boolean[] 765 @Link hoverArray: boolean[] 766 @Link hoverColorArray: HoverColorProperty[] 767 @Prop @Require maxFontScale: number | Resource 768 @State buttonWidth: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 0) 769 @State buttonHeight: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 0) 770 @State isMarqueeAndFadeout: boolean = false; 771 private buttonItemsRealHeight: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 0) 772 private groupId: string = util.generateRandomUUID(true) 773 public onItemClicked?: Callback<number> 774 @Prop isSegmentFocusStyleCustomized: boolean; 775 776 onButtonItemsSizeChange() { 777 this.buttonItemsSize.forEach((value, index) => { 778 this.buttonWidth[index] = value.width as number 779 this.buttonHeight[index] = value.height as number 780 }) 781 } 782 783 changeSelectedIndexes(buttonsLength: number) { 784 if (this.optionsArray.changeStartIndex === void 0 || this.optionsArray.deleteCount === void 0 || 785 this.optionsArray.addLength === void 0) { 786 return 787 } 788 if (!(this.options.multiply ?? false)) { 789 // Single-select 790 if (this.selectedIndexes[0] === void 0) { 791 return 792 } 793 if (this.selectedIndexes[0] < this.optionsArray.changeStartIndex) { 794 return 795 } 796 if (this.optionsArray.changeStartIndex + this.optionsArray.deleteCount > this.selectedIndexes[0]) { 797 if (this.options.type === 'tab') { 798 this.selectedIndexes[0] = 0 799 } else if (this.options.type === 'capsule') { 800 this.selectedIndexes = [] 801 } 802 } else { 803 this.selectedIndexes[0] = this.selectedIndexes[0] - this.optionsArray.deleteCount + this.optionsArray.addLength 804 } 805 } else { 806 // Multi-select 807 let saveIndexes = this.selectedIndexes 808 for (let i = 0; i < this.optionsArray.deleteCount; i++) { 809 let deleteIndex = saveIndexes.indexOf(this.optionsArray.changeStartIndex) 810 let indexes = saveIndexes.map(value => this.optionsArray.changeStartIndex && 811 (value > this.optionsArray.changeStartIndex) ? value - 1 : value) 812 if (deleteIndex !== -1) { 813 indexes.splice(deleteIndex, 1) 814 } 815 saveIndexes = indexes 816 } 817 for (let i = 0; i < this.optionsArray.addLength; i++) { 818 let indexes = saveIndexes.map(value => this.optionsArray.changeStartIndex && 819 (value >= this.optionsArray.changeStartIndex) ? value + 1 : value) 820 saveIndexes = indexes 821 } 822 this.selectedIndexes = saveIndexes 823 } 824 825 } 826 827 changeFocusIndex(buttonsLength: number) { 828 if (this.optionsArray.changeStartIndex === void 0 || this.optionsArray.deleteCount === void 0 || 829 this.optionsArray.addLength === void 0) { 830 return 831 } 832 if (this.focusIndex === -1) { 833 return 834 } 835 if (this.focusIndex < this.optionsArray.changeStartIndex) { 836 return 837 } 838 if (this.optionsArray.changeStartIndex + this.optionsArray.deleteCount > this.focusIndex) { 839 this.focusIndex = 0 840 } else { 841 this.focusIndex = this.focusIndex - this.optionsArray.deleteCount + this.optionsArray.addLength 842 } 843 844 } 845 846 onOptionsArrayChange() { 847 if (this.options === void 0 || this.options.buttons === void 0) { 848 return 849 } 850 let buttonsLength = Math.min(this.options.buttons.length, this.buttonItemsSize.length) 851 if (this.optionsArray.changeStartIndex !== void 0 && this.optionsArray.deleteCount !== void 0 && 852 this.optionsArray.addLength !== void 0) { 853 this.changeSelectedIndexes(buttonsLength) 854 this.changeFocusIndex(buttonsLength) 855 this.optionsArray.changeStartIndex = void 0 856 this.optionsArray.deleteCount = void 0 857 this.optionsArray.addLength = void 0 858 } 859 } 860 861 onOptionsChange() { 862 if (this.options === void 0 || this.options.buttons === void 0) { 863 return 864 } 865 this.calculateBorderRadius() 866 } 867 868 onFocusIndex(): void { 869 this.isMarqueeAndFadeout = this.isSegmentFocusStyleCustomized && !this.isMarqueeAndFadeout; 870 } 871 872 aboutToAppear() { 873 for (let index = 0; index < this.buttonItemsRealHeight.length; index++) { 874 this.buttonItemsRealHeight[index] = 0 875 } 876 } 877 878 private getFocusItemBorderRadius(index: number): LocalizedBorderRadiuses { 879 if (index < 0 || index >= this.buttonBorderRadius.length) { 880 return { 881 topStart: LengthMetrics.vp(0), 882 topEnd: LengthMetrics.vp(0), 883 bottomStart: LengthMetrics.vp(0), 884 bottomEnd: LengthMetrics.vp(0) 885 }; 886 } 887 888 let focusOffset = 0; 889 if (this.options.type === 'capsule' && 890 this.focusIndex >= 0 && 891 this.focusIndex < this.buttonItemsSelected.length && 892 this.buttonItemsSelected[this.focusIndex]) { 893 focusOffset = CAPSULE_FOCUS_SELECTED_OFFSET; 894 } 895 896 let borderRadius: LocalizedBorderRadiuses = this.buttonBorderRadius[index]; 897 898 return { 899 topStart: LengthMetrics.vp((borderRadius.topStart?.value ?? 0) + focusOffset), 900 topEnd: LengthMetrics.vp((borderRadius.topEnd?.value ?? 0) + focusOffset), 901 bottomStart: LengthMetrics.vp((borderRadius.bottomStart?.value ?? 0) + focusOffset), 902 bottomEnd: LengthMetrics.vp((borderRadius.bottomEnd?.value ?? 0) + focusOffset) 903 }; 904 } 905 906 private getFocusStackSize(index: number): SizeOptions { 907 const isCapsuleAndSelected = this.options.type === 'capsule' && 908 this.focusIndex >= 0 && 909 this.focusIndex < this.buttonItemsSelected.length && 910 this.buttonItemsSelected[this.focusIndex]; 911 912 return { 913 width: isCapsuleAndSelected 914 ? this.buttonWidth[index] + CAPSULE_FOCUS_SELECTED_OFFSET * 2 915 : this.buttonWidth[index], 916 height: isCapsuleAndSelected 917 ? this.buttonHeight[index] + CAPSULE_FOCUS_SELECTED_OFFSET * 2 918 : this.buttonHeight[index] 919 }; 920 } 921 922 @Builder 923 focusStack(index: number) { 924 Stack() { 925 Stack() 926 .direction(this.options.direction) 927 .borderRadius(this.getFocusItemBorderRadius(index)) 928 .size(this.getFocusStackSize(index)) 929 .borderColor(segmentButtonTheme.FOCUS_BORDER_COLOR) 930 .borderWidth(2) 931 } 932 .direction(this.options.direction) 933 .size({ width: 1, height: 1 }) 934 .align(Alignment.Center) 935 // 当前仅TV场景isSegmentFocusStyleCustomized为true,TV场景需要使用按键内置的focus样式,故隐藏此高级组件自定义的focus样式 936 .visibility(!this.isSegmentFocusStyleCustomized && this.focusIndex === index ? Visibility.Visible : Visibility.None) 937 } 938 939 calculateBorderRadius() { 940 // Calculate the border radius for each button 941 let borderRadiusArray: LocalizedBorderRadiuses[] = Array.from({ 942 length: MAX_ITEM_COUNT 943 }, (_: Object): LocalizedBorderRadiuses => { 944 return { 945 topStart: LengthMetrics.vp(0), 946 topEnd: LengthMetrics.vp(0), 947 bottomStart: LengthMetrics.vp(0), 948 bottomEnd: LengthMetrics.vp(0) 949 } 950 }); 951 952 const isSingleSelect = this.options.type === 'tab' || !(this.options.multiply ?? false); 953 const buttonsLength = 954 this.options.buttons ? Math.min(this.options.buttons.length, this.buttonItemsSize.length) : MIN_ITEM_COUNT; 955 956 const setAllCorners = (array: LocalizedBorderRadiuses[], index: number, lengthMetrics: LengthMetrics) => { 957 if (!array || index < 0 || index >= array.length) { 958 return; 959 } 960 961 const safeLengthMetrics = lengthMetrics.value < 0 ? LengthMetrics.vp(0) : lengthMetrics; 962 array[index].topStart = safeLengthMetrics; 963 array[index].topEnd = safeLengthMetrics; 964 array[index].bottomStart = safeLengthMetrics; 965 array[index].bottomEnd = safeLengthMetrics; 966 }; 967 968 const setLeftCorners = (array: LocalizedBorderRadiuses[], index: number, lengthMetrics: LengthMetrics) => { 969 if (!array || index < 0 || index >= array.length) { 970 return; 971 } 972 const safeLengthMetrics = lengthMetrics.value < 0 ? LengthMetrics.vp(0) : lengthMetrics; 973 const zeroLengthMetrics = LengthMetrics.vp(0); 974 array[index].topStart = safeLengthMetrics; 975 array[index].topEnd = zeroLengthMetrics; 976 array[index].bottomStart = safeLengthMetrics; 977 array[index].bottomEnd = zeroLengthMetrics; 978 }; 979 980 const setRightCorners = (array: LocalizedBorderRadiuses[], index: number, lengthMetrics: LengthMetrics) => { 981 if (!array || index < 0 || index >= array.length) { 982 return; 983 } 984 const safeLengthMetrics = lengthMetrics.value < 0 ? LengthMetrics.vp(0) : lengthMetrics; 985 const zeroLengthMetrics = LengthMetrics.vp(0); 986 array[index].topStart = zeroLengthMetrics; 987 array[index].topEnd = safeLengthMetrics; 988 array[index].bottomStart = zeroLengthMetrics; 989 array[index].bottomEnd = safeLengthMetrics; 990 }; 991 992 const setMiddleCorners = (array: LocalizedBorderRadiuses[], index: number) => { 993 if (!array || index < 0 || index >= array.length) { 994 return; 995 } 996 array[index].topStart = LengthMetrics.vp(0); 997 array[index].topEnd = LengthMetrics.vp(0); 998 array[index].bottomStart = LengthMetrics.vp(0); 999 array[index].bottomEnd = LengthMetrics.vp(0); 1000 }; 1001 1002 for (let index = 0; index < this.buttonBorderRadius.length; index++) { 1003 let halfButtonItemsSizeHeight = this.buttonItemsSize[index].height as number / 2; 1004 let radius = this.options.iconTextRadius ?? halfButtonItemsSizeHeight; // default radius 1005 // Determine which border radius to use based on mode setting 1006 const isCustomMode = this.options.borderRadiusMode === BorderRadiusMode.CUSTOM && 1007 this.options.itemBorderRadius !== undefined; 1008 1009 let radiusLengthMetrics: LengthMetrics; 1010 if (isCustomMode && this.options.itemBorderRadius) { 1011 // Use custom border radius from options 1012 radiusLengthMetrics = this.options.itemBorderRadius; 1013 } else { 1014 // Use default calculated radius value 1015 radiusLengthMetrics = LengthMetrics.vp(radius); 1016 } 1017 if (isSingleSelect) { 1018 // single-select 1019 setAllCorners(borderRadiusArray, index, radiusLengthMetrics); 1020 } else { 1021 // multi-select 1022 if (index === 0) { 1023 setLeftCorners(borderRadiusArray, index, radiusLengthMetrics); 1024 } else if (index === buttonsLength - 1) { 1025 setRightCorners(borderRadiusArray, index, radiusLengthMetrics); 1026 } else { 1027 setMiddleCorners(borderRadiusArray, index); 1028 } 1029 } 1030 } 1031 1032 this.buttonBorderRadius = borderRadiusArray; 1033 } 1034 1035 getAccessibilityDescription(value?: ResourceStr, index?: number): string | undefined { 1036 if (value !== undefined) { 1037 return value as string; 1038 } 1039 const isSingleSelect = this.options.type === 'tab' || !this.options.multiply; 1040 1041 if (isSingleSelect && index !== undefined && this.selectedIndexes.includes(index)) { 1042 return ACCESSIBILITY_SELECTED_DESCRIPTION; 1043 } 1044 1045 return ACCESSIBILITY_DEFAULT_DESCRIPTION; 1046 } 1047 1048 isDefaultSelectedBgColor(): boolean { 1049 if (this.options.type === 'tab') { 1050 return this.options.selectedBackgroundColor === segmentButtonTheme.TAB_SELECTED_BACKGROUND_COLOR; 1051 } else if (this.options.type === 'capsule') { 1052 return this.options.selectedBackgroundColor === segmentButtonTheme.CAPSULE_SELECTED_BACKGROUND_COLOR; 1053 } 1054 return true; 1055 } 1056 1057 build() { 1058 if (this.optionsArray !== void 0 && this.optionsArray.length > 1) { 1059 Row({ space: 1 }) { 1060 ForEach(this.optionsArray, (item: SegmentButtonItemOptions, index) => { 1061 if (index < MAX_ITEM_COUNT) { 1062 Button() { 1063 SegmentButtonItem({ 1064 isMarqueeAndFadeout: this.isMarqueeAndFadeout, 1065 isSegmentFocusStyleCustomized: this.isSegmentFocusStyleCustomized, 1066 selectedIndexes: $selectedIndexes, 1067 focusIndex: this.focusIndex, 1068 index: index, 1069 itemOptions: item, 1070 options: this.options, 1071 property: this.buttonItemProperty[index], 1072 groupId: this.groupId, 1073 maxFontScale: this.maxFontScale, 1074 hover: this.hoverArray[index], 1075 }) 1076 .onSizeChange((_, newValue) => { 1077 // Calculate height of items 1078 this.buttonItemsRealHeight[index] = newValue.height as number 1079 let maxHeight = Math.max(...this.buttonItemsRealHeight.slice(0, this.options.buttons ? 1080 this.options.buttons.length : 0)) 1081 for (let index = 0; index < this.buttonItemsSize.length; index++) { 1082 this.buttonItemsSize[index] = { width: this.buttonItemsSize[index].width, height: maxHeight } 1083 } 1084 this.calculateBorderRadius() 1085 }) 1086 } 1087 .focusScopePriority(this.groupId, 1088 Math.min(...this.selectedIndexes) === index ? FocusPriority.PREVIOUS : FocusPriority.AUTO) 1089 .type(ButtonType.Normal) 1090 .stateEffect(false) 1091 .hoverEffect(HoverEffect.None) 1092 .backgroundColor(Color.Transparent) 1093 .accessibilityLevel(item.accessibilityLevel) 1094 .accessibilitySelected(this.options.multiply ? undefined : this.selectedIndexes.includes(index)) 1095 .accessibilityChecked(this.options.multiply ? this.selectedIndexes.includes(index) : undefined) 1096 .accessibilityDescription(this.getAccessibilityDescription(item.accessibilityDescription, index)) 1097 .direction(this.options.direction) 1098 .borderRadius(this.buttonBorderRadius[index]) 1099 .scale({ 1100 x: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index], 1101 y: this.options.type === 'capsule' && (this.options.multiply ?? false) ? 1 : this.zoomScaleArray[index] 1102 }) 1103 .layoutWeight(1) 1104 .padding(0) 1105 .onSizeChange((_, newValue) => { 1106 this.buttonItemsSize[index] = { width: newValue.width, height: this.buttonItemsSize[index].height } 1107 //measure position 1108 if (newValue.width) { 1109 this.buttonItemsPosition[index] = { 1110 start: LengthMetrics.vp(Number.parseFloat(this.options.componentPadding.toString()) + 1111 (Number.parseFloat(newValue.width.toString()) + 1) * index), 1112 top: LengthMetrics.px(Math.floor(this.getUIContext() 1113 .vp2px(Number.parseFloat(this.options.componentPadding.toString())))) 1114 } 1115 } 1116 }) 1117 .overlay(this.focusStack(index), { align: Alignment.Center }) 1118 .attributeModifier(this.isSegmentFocusStyleCustomized ? undefined : 1119 new FocusStyleButtonModifier((isFocused: boolean): void => { 1120 if (!isFocused && this.focusIndex === index) { 1121 this.focusIndex = -1; 1122 return; 1123 } 1124 if (isFocused) { 1125 this.focusIndex = index; 1126 } 1127 })) 1128 .onFocus(() => { 1129 this.focusIndex = index; 1130 if (this.isSegmentFocusStyleCustomized) { 1131 this.customizeSegmentFocusStyle(index); 1132 } 1133 }) 1134 .onBlur(() => { 1135 if (this.focusIndex === index) { 1136 this.focusIndex = -1; 1137 } 1138 this.hoverColorArray[index].hoverColor = Color.Transparent; 1139 }) 1140 .gesture(TapGesture().onAction(() => { 1141 if (this.onItemClicked) { 1142 this.onItemClicked(index) 1143 } 1144 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1145 if (this.selectedIndexes.indexOf(index) === -1) { 1146 this.selectedIndexes.push(index) 1147 } else { 1148 this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1) 1149 } 1150 } else { 1151 this.selectedIndexes[0] = index 1152 } 1153 })) 1154 .onTouch((event: TouchEvent) => { 1155 if (this.isSegmentFocusStyleCustomized) { 1156 this.getUIContext().getFocusController().clearFocus(); 1157 } 1158 if (event.source !== SourceType.TouchScreen) { 1159 return 1160 } 1161 if (event.type === TouchType.Down) { 1162 animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 1163 this.zoomScaleArray[index] = 0.95 1164 }) 1165 } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { 1166 animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 1167 this.zoomScaleArray[index] = 1 1168 }) 1169 } 1170 }) 1171 .onHover((isHover: boolean) => { 1172 this.hoverArray[index] = isHover 1173 if (isHover) { 1174 animateTo({ duration: 250, curve: Curve.Friction }, () => { 1175 this.hoverColorArray[index].hoverColor = 1176 this.isSegmentFocusStyleCustomized && this.focusIndex === index ? 1177 segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : segmentButtonTheme.HOVER_COLOR; 1178 }) 1179 } else { 1180 animateTo({ duration: 250, curve: Curve.Friction }, () => { 1181 this.hoverColorArray[index].hoverColor = 1182 this.isSegmentFocusStyleCustomized && this.focusIndex === index ? 1183 segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : Color.Transparent; 1184 }) 1185 } 1186 }) 1187 .onMouse((event: MouseEvent) => { 1188 switch (event.action) { 1189 case MouseAction.Press: 1190 animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => { 1191 this.zoomScaleArray[index] = 0.95 1192 }) 1193 animateTo({ duration: 100, curve: Curve.Sharp }, () => { 1194 this.pressArray[index] = true 1195 }) 1196 break; 1197 case MouseAction.Release: 1198 animateTo({ curve: curves.springMotion(0.347, 0.99) }, () => { 1199 this.zoomScaleArray[index] = 1 1200 }) 1201 animateTo({ duration: 100, curve: Curve.Sharp }, () => { 1202 this.pressArray[index] = false 1203 }) 1204 break; 1205 } 1206 }) 1207 } 1208 }) 1209 } 1210 .direction(this.options.direction) 1211 .focusScopeId(this.groupId, true) 1212 .padding(this.options.componentPadding) 1213 .onSizeChange((_, newValue) => { 1214 this.componentSize = { width: newValue.width, height: newValue.height } 1215 }) 1216 } 1217 } 1218 1219 /** 1220 * 设置segmentbutton获焦时的样式 1221 * @param index 1222 */ 1223 private customizeSegmentFocusStyle(index: number) { 1224 if (this.selectedIndexes !== void 0 && this.selectedIndexes.length !== 0 && 1225 this.selectedIndexes[0] === index) { // 选中态 1226 this.hoverColorArray[index].hoverColor = this.isDefaultSelectedBgColor() ? 1227 segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : this.options.selectedBackgroundColor; 1228 } else { // 未选中态 1229 this.hoverColorArray[index].hoverColor = this.options.backgroundColor === segmentButtonTheme.BACKGROUND_COLOR ? 1230 segmentButtonTheme.SEGMENT_BUTTON_FOCUS_CUSTOMIZED_BG_COLOR : this.options.backgroundColor; 1231 } 1232 } 1233} 1234 1235@Observed 1236class ItemProperty { 1237 public fontColor: ResourceColor = segmentButtonTheme.FONT_COLOR 1238 public fontSize: DimensionNoPercentage = segmentButtonTheme.FONT_SIZE 1239 public fontWeight: FontWeight = FontWeight.Regular 1240 public isSelected: boolean = false 1241} 1242 1243@Component 1244export struct SegmentButton { 1245 @ObjectLink @Watch('onOptionsChange') options: SegmentButtonOptions 1246 @Link @Watch('onSelectedChange') selectedIndexes: number[] 1247 public onItemClicked?: Callback<number> 1248 @Prop maxFontScale: number | Resource = DEFAULT_MAX_FONT_SCALE 1249 @Provide componentSize: SizeOptions = { width: 0, height: 0 } 1250 @Provide buttonBorderRadius: LocalizedBorderRadiuses[] = Array.from({ 1251 length: MAX_ITEM_COUNT 1252 }, (_: Object, index): LocalizedBorderRadiuses => { 1253 return { 1254 topStart: LengthMetrics.vp(0), 1255 topEnd: LengthMetrics.vp(0), 1256 bottomStart: LengthMetrics.vp(0), 1257 bottomEnd: LengthMetrics.vp(0) 1258 } 1259 }) 1260 @Provide buttonItemsSize: SizeOptions[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index): SizeOptions => { 1261 return {} 1262 }) 1263 @Provide @Watch('onItemsPositionChange') buttonItemsPosition: LocalizedEdges[] = Array.from({ 1264 length: MAX_ITEM_COUNT 1265 }, (_: Object, index): LocalizedEdges => { 1266 return {} 1267 }) 1268 @Provide buttonItemsSelected: boolean[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => false) 1269 @Provide buttonItemProperty: ItemProperty[] = Array.from({ 1270 length: MAX_ITEM_COUNT 1271 }, (_: Object, index) => new ItemProperty()) 1272 @Provide focusIndex: number = -1 1273 @Provide selectedItemPosition: LocalizedEdges = {} 1274 @Provide zoomScaleArray: number[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => 1.0) 1275 @State pressArray: boolean[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => false) 1276 @State hoverArray: boolean[] = Array.from({ length: MAX_ITEM_COUNT }, (_: Object, index) => false) 1277 @State hoverColorArray: HoverColorProperty[] = Array.from({ 1278 length: MAX_ITEM_COUNT 1279 }, (_: Object, index) => new HoverColorProperty()) 1280 private doSelectedChangeAnimate: boolean = false 1281 private isCurrentPositionSelected: boolean = false 1282 private panGestureStartPoint: Point = { x: 0, y: 0 } 1283 private isPanGestureMoved: boolean = false 1284 @State shouldMirror: boolean = false 1285 private isGestureInProgress: boolean = false; 1286 private isCustomizedCache?: boolean; 1287 1288 onItemsPositionChange() { 1289 if (this.options === void 0 || this.options.buttons === void 0) { 1290 return 1291 } 1292 if (this.options.type === 'capsule') { 1293 this.options.onButtonsUpdated(); 1294 } 1295 if (this.doSelectedChangeAnimate) { 1296 this.updateAnimatedProperty(this.getSelectedChangeCurve()) 1297 } else { 1298 this.updateAnimatedProperty(null) 1299 } 1300 } 1301 1302 setItemsSelected() { 1303 this.buttonItemsSelected.forEach((_, index) => { 1304 this.buttonItemsSelected[index] = false 1305 }) 1306 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1307 this.selectedIndexes.forEach(index => this.buttonItemsSelected[index] = true) 1308 } else { 1309 this.buttonItemsSelected[this.selectedIndexes[0]] = true 1310 } 1311 } 1312 1313 updateSelectedIndexes() { 1314 if (this.selectedIndexes === void 0) { 1315 this.selectedIndexes = [] 1316 } 1317 if (this.options.type === 'tab' && this.selectedIndexes.length === 0) { 1318 this.selectedIndexes[0] = 0 1319 } 1320 if (this.selectedIndexes.length > 1) { 1321 if (this.options.type === 'tab') { 1322 this.selectedIndexes = [0] 1323 } 1324 if (this.options.type === 'capsule' && !(this.options.multiply ?? false)) { 1325 this.selectedIndexes = [] 1326 } 1327 } 1328 let invalid = this.selectedIndexes.some(index => { 1329 return (index === void 0 || index < 0 || (this.options.buttons && index >= this.options.buttons.length)) 1330 }) 1331 if (invalid) { 1332 if (this.options.type === 'tab') { 1333 this.selectedIndexes = [0] 1334 } else { 1335 this.selectedIndexes = [] 1336 } 1337 } 1338 } 1339 1340 onOptionsChange() { 1341 if (this.options === void 0 || this.options.buttons === void 0) { 1342 return 1343 } 1344 this.shouldMirror = this.isShouldMirror() 1345 this.updateSelectedIndexes() 1346 this.setItemsSelected() 1347 this.updateAnimatedProperty(null) 1348 } 1349 1350 onSelectedChange() { 1351 if (this.options === void 0 || this.options.buttons === void 0) { 1352 return 1353 } 1354 this.updateSelectedIndexes() 1355 this.setItemsSelected() 1356 if (this.doSelectedChangeAnimate) { 1357 this.updateAnimatedProperty(this.getSelectedChangeCurve()) 1358 } else { 1359 this.updateAnimatedProperty(null) 1360 } 1361 } 1362 1363 aboutToAppear() { 1364 if (this.options === void 0 || this.options.buttons === void 0) { 1365 return 1366 } 1367 this.options.onButtonsChange = () => { 1368 if (this.options.type === 'tab') { 1369 this.selectedIndexes = [0] 1370 } else { 1371 this.selectedIndexes = [] 1372 } 1373 } 1374 this.shouldMirror = this.isShouldMirror() 1375 this.updateSelectedIndexes() 1376 this.setItemsSelected() 1377 this.updateAnimatedProperty(null) 1378 } 1379 1380 private isMouseWheelScroll(event: GestureEvent) { 1381 return event.source === SourceType.Mouse && !this.isPanGestureMoved 1382 } 1383 1384 private isMovedFromPanGestureStartPoint(x: number, y: number) { 1385 return !nearEqual(x, this.panGestureStartPoint.x) || !nearEqual(y, this.panGestureStartPoint.y) 1386 } 1387 1388 private isShouldMirror(): boolean { 1389 if (this.options.direction == Direction.Rtl) { 1390 return true 1391 } 1392 // 获取系统语言 1393 try { 1394 let systemLanguage: string = I18n.System.getSystemLanguage(); 1395 if (I18n.isRTL(systemLanguage) && this.options.direction != Direction.Ltr) { 1396 return true 1397 } 1398 } catch (error) { 1399 console.error(`Ace SegmentButton getSystemLanguage, error: ${error.toString()}`); 1400 } 1401 return false 1402 } 1403 1404 private isSegmentFocusStyleCustomized(): boolean { 1405 if (this.isCustomizedCache === undefined) { 1406 this.isCustomizedCache = resourceToNumber( 1407 this.getUIContext()?.getHostContext(), 1408 segmentButtonTheme.SEGMENT_FOCUS_STYLE_CUSTOMIZED, 1409 1.0 1410 ) < 0.1; //PC platform returns 0.0, default returns 1.0, using <0.1 to differentiate platform styles. 1411 } 1412 return this.isCustomizedCache; 1413 } 1414 1415 build() { 1416 Stack() { 1417 if (this.options !== void 0 && this.options.buttons != void 0) { 1418 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1419 MultiSelectBackground({ 1420 optionsArray: this.options.buttons, 1421 options: this.options, 1422 }) 1423 } else { 1424 Stack() { 1425 if (this.options.buttons !== void 0 && this.options.buttons.length > 1) { 1426 PressAndHoverEffectArray({ 1427 options: this.options, 1428 buttons: this.options.buttons, 1429 pressArray: this.pressArray, 1430 hoverArray: this.hoverArray, 1431 hoverColorArray: this.hoverColorArray 1432 }) 1433 } 1434 } 1435 .direction(this.options.direction) 1436 .size(this.componentSize) 1437 .backgroundColor(this.options.backgroundColor ?? segmentButtonTheme.BACKGROUND_COLOR) 1438 .borderRadius(getBackgroundBorderRadius( 1439 this.options, 1440 this.componentSize.height as number / 2 1441 )) 1442 .backgroundBlurStyle(this.options.backgroundBlurStyle, undefined, { disableSystemAdaptation: true }) 1443 } 1444 Stack() { 1445 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1446 MultiSelectItemArray({ 1447 optionsArray: this.options.buttons, 1448 options: this.options, 1449 selectedIndexes: $selectedIndexes 1450 }) 1451 } else { 1452 SelectItem({ 1453 optionsArray: this.options.buttons, 1454 options: this.options, 1455 selectedIndexes: $selectedIndexes 1456 }) 1457 } 1458 } 1459 .direction(this.options.direction) 1460 .size(this.componentSize) 1461 .animation({ duration: 0 }) 1462 .borderRadius(getBackgroundBorderRadius( 1463 this.options, 1464 this.componentSize.height as number / 2 1465 )) 1466 .clip(true) 1467 1468 SegmentButtonItemArrayComponent({ 1469 pressArray: this.pressArray, 1470 hoverArray: this.hoverArray, 1471 hoverColorArray: this.hoverColorArray, 1472 optionsArray: this.options.buttons, 1473 options: this.options, 1474 selectedIndexes: $selectedIndexes, 1475 maxFontScale: this.getMaxFontSize(), 1476 onItemClicked: this.onItemClicked, 1477 isSegmentFocusStyleCustomized: this.isSegmentFocusStyleCustomized() 1478 }) 1479 } 1480 } 1481 .direction(this.options ? this.options.direction : undefined) 1482 .onBlur(() => { 1483 this.focusIndex = -1 1484 }) 1485 .onKeyEvent((event: KeyEvent) => { 1486 if (this.options === void 0 || this.options.buttons === void 0) { 1487 return 1488 } 1489 if (event.type === KeyType.Down) { 1490 if (event.keyCode === KeyCode.KEYCODE_SPACE || event.keyCode === KeyCode.KEYCODE_ENTER || 1491 event.keyCode === KeyCode.KEYCODE_NUMPAD_ENTER) { 1492 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1493 if (this.selectedIndexes.indexOf(this.focusIndex) === -1) { 1494 // Select 1495 this.selectedIndexes.push(this.focusIndex) 1496 } else { 1497 // Unselect 1498 this.selectedIndexes.splice(this.selectedIndexes.indexOf(this.focusIndex), 1) 1499 } 1500 } else { 1501 // Pressed 1502 this.selectedIndexes[0] = this.focusIndex 1503 } 1504 } 1505 } 1506 }) 1507 .accessibilityLevel('no') 1508 .priorityGesture( 1509 GestureGroup(GestureMode.Parallel, 1510 TapGesture() 1511 .onAction((event: GestureEvent) => { 1512 if (this.isGestureInProgress) { 1513 return; 1514 } 1515 let fingerInfo = event.fingerList.find(Boolean) 1516 if (fingerInfo === void 0) { 1517 return 1518 } 1519 if (this.options === void 0 || this.options.buttons === void 0) { 1520 return 1521 } 1522 let selectedInfo = fingerInfo.localX 1523 1524 let buttonLength: number = Math.min(this.options.buttons.length, this.buttonItemsSize.length) 1525 for (let i = 0; i < buttonLength; i++) { 1526 selectedInfo = selectedInfo - (this.buttonItemsSize[i].width as number) 1527 if (selectedInfo >= 0) { 1528 continue 1529 } 1530 this.doSelectedChangeAnimate = 1531 this.selectedIndexes[0] > Math.min(this.options.buttons.length, 1532 this.buttonItemsSize.length) ? false : true 1533 1534 let realClickIndex: number = this.isShouldMirror() ? buttonLength - 1 - i : i 1535 if (this.onItemClicked) { 1536 this.onItemClicked(realClickIndex) 1537 } 1538 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1539 let selectedIndex: number = this.selectedIndexes.indexOf(realClickIndex) 1540 if (selectedIndex === -1) { 1541 this.selectedIndexes.push(realClickIndex) 1542 } else { 1543 this.selectedIndexes.splice(selectedIndex, 1) 1544 } 1545 } else { 1546 this.selectedIndexes[0] = realClickIndex 1547 } 1548 this.doSelectedChangeAnimate = false 1549 break 1550 } 1551 }), 1552 SwipeGesture() 1553 .onAction((event: GestureEvent) => { 1554 if (this.options === void 0 || this.options.buttons === void 0 || 1555 event.sourceTool === SourceTool.TOUCHPAD) { 1556 return 1557 } 1558 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1559 // Non swipe gesture in multi-select mode 1560 return 1561 } 1562 if (this.isCurrentPositionSelected) { 1563 return 1564 } 1565 1566 // Only handle horizontal swipes (angle between -45 to 45 degrees or 135 to 225 degrees) 1567 let isHorizontalSwipe = (Math.abs(event.angle) <= 45) || (Math.abs(event.angle) >= 135); 1568 if (!isHorizontalSwipe) { 1569 return; 1570 } 1571 1572 let isSwipeRight = Math.abs(event.angle) <= 45; // swipe right 1573 let isSwipeLeft = Math.abs(event.angle) >= 135; // swipe left 1574 1575 let isSwipeToNext = this.isShouldMirror() ? isSwipeLeft : isSwipeRight; 1576 let isSwipeToPrevious = this.isShouldMirror() ? isSwipeRight : isSwipeLeft; 1577 1578 if (isSwipeToNext && this.selectedIndexes[0] !== Math.min(this.options.buttons.length, 1579 this.buttonItemsSize.length) - 1) { 1580 // Move to next 1581 this.doSelectedChangeAnimate = true 1582 this.selectedIndexes[0] = this.selectedIndexes[0] + 1 1583 this.doSelectedChangeAnimate = false 1584 } else if (isSwipeToPrevious && this.selectedIndexes[0] !== 0) { 1585 // Move to previous 1586 this.doSelectedChangeAnimate = true 1587 this.selectedIndexes[0] = this.selectedIndexes[0] - 1 1588 this.doSelectedChangeAnimate = false 1589 } 1590 }), 1591 PanGesture({direction: PanDirection.Horizontal}) 1592 .onActionStart((event: GestureEvent) => { 1593 this.isGestureInProgress = true; 1594 if (this.options === void 0 || this.options.buttons === void 0) { 1595 return 1596 } 1597 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1598 // Non drag gesture in multi-select mode 1599 return 1600 } 1601 let fingerInfo = event.fingerList.find(Boolean) 1602 if (fingerInfo === void 0) { 1603 return 1604 } 1605 let selectedInfo = fingerInfo.localX 1606 this.panGestureStartPoint = { x: fingerInfo.globalX, y: fingerInfo.globalY } 1607 this.isPanGestureMoved = false 1608 1609 let buttonLength: number = Math.min(this.options.buttons.length, this.buttonItemsSize.length); 1610 for (let i = 0; i < buttonLength; i++) { 1611 selectedInfo = selectedInfo - (this.buttonItemsSize[i].width as number) 1612 if (selectedInfo < 0) { 1613 let realIndex = this.isShouldMirror() ? buttonLength - 1 - i : i; 1614 this.isCurrentPositionSelected = realIndex === this.selectedIndexes[0] ? true : false; 1615 break 1616 } 1617 } 1618 }) 1619 .onActionUpdate((event: GestureEvent) => { 1620 if (this.options === void 0 || this.options.buttons === void 0) { 1621 return 1622 } 1623 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1624 // Non drag gesture in multi-select mode 1625 return 1626 } 1627 if (!this.isCurrentPositionSelected) { 1628 return 1629 } 1630 let fingerInfo = event.fingerList.find(Boolean) 1631 if (fingerInfo === void 0) { 1632 return 1633 } 1634 let selectedInfo = fingerInfo.localX 1635 if (!this.isPanGestureMoved && this.isMovedFromPanGestureStartPoint(fingerInfo.globalX, 1636 fingerInfo.globalY)) { 1637 this.isPanGestureMoved = true 1638 } 1639 1640 let buttonLength: number = Math.min(this.options.buttons.length, this.buttonItemsSize.length); 1641 for (let i = 0; i < buttonLength; i++) { 1642 selectedInfo = selectedInfo - (this.buttonItemsSize[i].width as number); 1643 if (selectedInfo < 0) { 1644 let realIndex = this.isShouldMirror() ? buttonLength - 1 - i : i; 1645 this.doSelectedChangeAnimate = true; 1646 this.selectedIndexes[0] = realIndex; 1647 this.doSelectedChangeAnimate = false; 1648 break; 1649 } 1650 } 1651 this.zoomScaleArray.forEach((_, index) => { 1652 if (index === this.selectedIndexes[0]) { 1653 animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 1654 this.zoomScaleArray[index] = 0.95 1655 }) 1656 } else { 1657 animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 1658 this.zoomScaleArray[index] = 1 1659 }) 1660 } 1661 }) 1662 }) 1663 .onActionEnd((event: GestureEvent) => { 1664 this.isGestureInProgress = false; 1665 if (this.options === void 0 || this.options.buttons === void 0) { 1666 return 1667 } 1668 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1669 // Non drag gesture in multi-select mode 1670 return 1671 } 1672 let fingerInfo = event.fingerList.find(Boolean) 1673 if (fingerInfo === void 0) { 1674 return 1675 } 1676 if (!this.isPanGestureMoved && this.isMovedFromPanGestureStartPoint(fingerInfo.globalX, 1677 fingerInfo.globalY)) { 1678 this.isPanGestureMoved = true 1679 } 1680 if (this.isMouseWheelScroll(event)) { 1681 let offset = event.offsetX !== 0 ? event.offsetX : event.offsetY 1682 this.doSelectedChangeAnimate = true 1683 1684 // Reverse mouse wheel direction in mirrored layout 1685 let shouldMoveNext = this.isShouldMirror() ? offset > 0 : offset < 0; 1686 let shouldMovePrevious = this.isShouldMirror() ? offset < 0 : offset > 0; 1687 1688 if (shouldMovePrevious && this.selectedIndexes[0] > 0) { 1689 this.selectedIndexes[0] -= 1; 1690 } else if (shouldMoveNext && this.selectedIndexes[0] < Math.min(this.options.buttons.length, 1691 this.buttonItemsSize.length) - 1) { 1692 this.selectedIndexes[0] += 1 1693 } 1694 this.doSelectedChangeAnimate = false 1695 } 1696 animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 1697 this.zoomScaleArray[this.selectedIndexes[0]] = 1 1698 }) 1699 this.isCurrentPositionSelected = false 1700 }) 1701 .onActionCancel(() => { 1702 this.isGestureInProgress = false; 1703 if (this.options === void 0 || this.options.buttons === void 0) { 1704 return 1705 } 1706 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1707 return 1708 } 1709 animateTo({ curve: curves.interpolatingSpring(10, 1, 410, 38) }, () => { 1710 this.zoomScaleArray[this.selectedIndexes[0]] = 1 1711 }) 1712 this.isCurrentPositionSelected = false 1713 }) 1714 ) 1715 ) 1716 } 1717 1718 getMaxFontSize(): number { 1719 if (typeof this.maxFontScale === void 0) { 1720 return DEFAULT_MAX_FONT_SCALE; 1721 } 1722 if (typeof this.maxFontScale === 'number') { 1723 return Math.max(Math.min(this.maxFontScale, MAX_MAX_FONT_SCALE), MIN_MAX_FONT_SCALE); 1724 } 1725 const resourceManager = this.getUIContext().getHostContext()?.resourceManager; 1726 if (!resourceManager) { 1727 return DEFAULT_MAX_FONT_SCALE; 1728 } 1729 try { 1730 return resourceManager.getNumber(this.maxFontScale.id); 1731 } catch (error) { 1732 console.error(`Ace SegmentButton getMaxFontSize, error: ${error.toString()}`); 1733 return DEFAULT_MAX_FONT_SCALE; 1734 } 1735 } 1736 1737 getSelectedChangeCurve(): ICurve | null { 1738 if (this.options.type === 'capsule' && (this.options.multiply ?? false)) { 1739 return null 1740 } 1741 return curves.springMotion(0.347, 0.99) 1742 } 1743 1744 updateAnimatedProperty(curve: ICurve | null) { 1745 let setAnimatedPropertyFunc = () => { 1746 this.selectedItemPosition = 1747 this.selectedIndexes.length === 0 ? {} : this.buttonItemsPosition[this.selectedIndexes[0]] 1748 this.buttonItemsSelected.forEach((selected, index) => { 1749 this.buttonItemProperty[index].fontColor = selected ? 1750 this.options.selectedFontColor ?? (this.options.type === 'tab' ? 1751 segmentButtonTheme.TAB_SELECTED_FONT_COLOR : segmentButtonTheme.CAPSULE_SELECTED_FONT_COLOR) : 1752 this.options.fontColor ?? segmentButtonTheme.FONT_COLOR 1753 }) 1754 } 1755 if (curve) { 1756 animateTo({ curve: curve }, setAnimatedPropertyFunc) 1757 } else { 1758 setAnimatedPropertyFunc() 1759 } 1760 this.buttonItemsSelected.forEach((selected, index) => { 1761 this.buttonItemProperty[index].fontSize = selected ? this.options.selectedFontSize ?? 1762 segmentButtonTheme.SELECTED_FONT_SIZE : this.options.fontSize ?? segmentButtonTheme.FONT_SIZE 1763 this.buttonItemProperty[index].fontWeight = selected ? this.options.selectedFontWeight ?? FontWeight.Medium : 1764 this.options.fontWeight ?? FontWeight.Regular 1765 this.buttonItemProperty[index].isSelected = selected 1766 }) 1767 } 1768} 1769 1770function resourceToNumber(context: Context | undefined, resource: Resource, defaultValue: number): number { 1771 if (!resource || !resource.type || !context) { 1772 console.error('[SegmentButton] failed: resource get fail.'); 1773 return defaultValue; 1774 } 1775 let resourceManager = context?.resourceManager; 1776 if (!resourceManager) { 1777 console.error('[SegmentButton] failed to get resourceManager.'); 1778 return defaultValue; 1779 } 1780 switch (resource.type) { 1781 case RESOURCE_TYPE_FLOAT: 1782 case RESOURCE_TYPE_INTEGER: 1783 try { 1784 if (resource.id !== -1) { 1785 return resourceManager.getNumber(resource); 1786 } 1787 return resourceManager.getNumberByName((resource.params as string[])[0].split('.')[2]); 1788 } catch (error) { 1789 console.error(`[SegmentButton] get resource error, return defaultValue`); 1790 return defaultValue; 1791 } 1792 default: 1793 return defaultValue; 1794 } 1795} 1796 1797class LengthMetricsUtils { 1798 private static instance?: LengthMetricsUtils; 1799 1800 private constructor() { 1801 } 1802 1803 public static getInstance(): LengthMetricsUtils { 1804 if (!LengthMetricsUtils.instance) { 1805 LengthMetricsUtils.instance = new LengthMetricsUtils(); 1806 } 1807 return LengthMetricsUtils.instance; 1808 } 1809 1810 stringify(metrics: LengthMetrics): Dimension { 1811 switch (metrics.unit) { 1812 case LengthUnit.PX: 1813 return `${metrics.value}px`; 1814 case LengthUnit.VP: 1815 return `${metrics.value}vp`; 1816 case LengthUnit.FP: 1817 return `${metrics.value}fp`; 1818 case LengthUnit.PERCENT: 1819 return `${metrics.value}%`; 1820 case LengthUnit.LPX: 1821 return `${metrics.value}lpx`; 1822 } 1823 } 1824 1825 isNaturalNumber(metrics: LengthMetrics): boolean { 1826 return metrics.value >= 0; 1827 } 1828} 1829 1830function getBackgroundBorderRadius(options: SegmentButtonOptions, defaultRadius: number): Length { 1831 if (options.borderRadiusMode === BorderRadiusMode.CUSTOM) { 1832 // For capsule multi-select buttons, use itemBorderRadius 1833 if (options.type === 'capsule' && (options.multiply ?? false) && options.itemBorderRadius !== undefined) { 1834 return LengthMetricsUtils.getInstance().stringify(options.itemBorderRadius); 1835 } else if (options.backgroundBorderRadius !== undefined) { 1836 return LengthMetricsUtils.getInstance().stringify(options.backgroundBorderRadius); 1837 } 1838 } 1839 1840 if (options.type === 'capsule' && (options.multiply ?? false)) { 1841 return options.iconTextRadius ?? options.iconTextBackgroundRadius ?? defaultRadius; 1842 } 1843 return options.iconTextBackgroundRadius ?? defaultRadius; 1844} 1845 1846class FocusStyleButtonModifier implements AttributeModifier<ButtonAttribute> { 1847 private stateStyleAction?: (isFocused: boolean) => void; 1848 1849 constructor(stateStyleAction: (isFocused: boolean) => void) { 1850 this.stateStyleAction = stateStyleAction; 1851 } 1852 1853 applyNormalAttribute(instance: ButtonAttribute): void { 1854 this.stateStyleAction && this.stateStyleAction(false); 1855 } 1856 1857 applyFocusedAttribute(instance: ButtonAttribute): void { 1858 this.stateStyleAction && this.stateStyleAction(true); 1859 } 1860}