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