• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}