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