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 */
15
16import { Theme } from '@ohos.arkui.theme';
17import { LengthMetrics } from '@ohos.arkui.node';
18import common from '@ohos.app.ability.common';
19
20export enum IconType {
21  BADGE = 1,
22  NORMAL_ICON,
23  SYSTEM_ICON,
24  HEAD_SCULPTURE,
25  APP_ICON,
26  PREVIEW,
27  LONGITUDINAL,
28  VERTICAL
29}
30
31enum FontSizeScaleLevel {
32  LEVEL1 = 1.75,
33  LEVEL2 = 2,
34  LEVEL3 = 3.2
35}
36
37enum ItemHeight {
38  FIRST_HEIGHT = 48,
39  SECOND_HEIGHT = 56,
40  THIRD_HEIGHT = 64,
41  FOURTH_HEIGHT = 72,
42  FIFTH_HEIGHT = 96
43}
44
45export declare class OperateItem {
46  public icon?: OperateIcon;
47  public subIcon?: OperateIcon;
48  public button?: OperateButton;
49  public switch?: OperateCheck;
50  public checkbox?: OperateCheck;
51  public radio?: OperateCheck;
52  public image?: ResourceStr;
53  public text?: ResourceStr;
54  public arrow?: OperateIcon;
55}
56
57export declare class ContentItem {
58  public iconStyle?: IconType;
59  public icon?: ResourceStr;
60  public primaryText?: ResourceStr;
61  public secondaryText?: ResourceStr;
62  public description?: ResourceStr;
63}
64
65export declare class OperateIcon {
66  public value: ResourceStr;
67  public action?: () => void;
68}
69
70export declare class OperateButton {
71  public text?: ResourceStr;
72}
73
74export declare class OperateCheck {
75  public isCheck?: boolean;
76  public onChange?: (value: boolean) => void;
77}
78
79const TEXT_MAX_LINE = 1;
80const ITEM_BORDER_SHOWN = 2;
81const TEXT_COLUMN_SPACE = 4;
82const TEXT_SAFE_MARGIN = 8;
83const LISTITEM_PADDING = 6;
84const SWITCH_PADDING = 4;
85const STACK_PADDING = 4;
86const BADGE_SIZE = 8;
87const SMALL_ICON_SIZE = 16;
88const SYSTEM_ICON_SIZE = 24;
89const TEXT_ARROW_HEIGHT = 32;
90const SAFE_LIST_PADDING = 32;
91const HEADSCULPTURE_SIZE = 40;
92const BUTTON_SIZE = 28;
93const APP_ICON_SIZE = 64;
94const PREVIEW_SIZE = 96;
95const LONGITUDINAL_SIZE = 96;
96const VERTICAL_SIZE = 96;
97const NORMAL_ITEM_ROW_SPACE = 16;
98const SPECIAL_ITEM_ROW_SPACE = 0;
99const SPECIAL_ICON_SIZE = 0;
100const DEFAULT_ROW_SPACE = 0;
101const SPECICAL_ROW_SPACE = 4;
102const OPERATEITEM_ICONLIKE_SIZE = 24;
103const OPERATEITEM_SELECTIONBOX_PADDING_SIZE = 2;
104const OPERATEITEM_ARROW_WIDTH = 12
105const OPERATEITEM_ICON_CLICKABLE_SIZE = 40;
106const OPERATEITEM_IMAGE_SIZE = 48;
107const RIGHT_CONTENT_NULL_RIGHTWIDTH = '0vp';
108const LEFT_PART_WIDTH = 'calc(66% - 16vp)';
109const RIGHT_PART_WIDTH = '34%';
110const RIGHT_ONLY_ARROW_WIDTH = '24vp';
111const RIGHT_ONLY_IMAGE_WIDTH = '54vp';
112const RIGHT_ONLY_ICON_WIDTH = '40vp';
113const RIGHT_ICON_SUB_ICON_WIDTH = '80vp';
114const RIGHT_ONLY_RADIO_WIDTH = '30vp';
115const RIGHT_ONLY_CHECKBOX_WIDTH = '30vp';
116const RIGHT_ONLY_SWITCH_WIDTH = '44vp';
117
118const ICON_SIZE_MAP: Map<number, number> = new Map([
119  [IconType.BADGE, BADGE_SIZE],
120  [IconType.NORMAL_ICON, SMALL_ICON_SIZE],
121  [IconType.SYSTEM_ICON, SYSTEM_ICON_SIZE],
122  [IconType.HEAD_SCULPTURE, HEADSCULPTURE_SIZE],
123  [IconType.APP_ICON, APP_ICON_SIZE],
124  [IconType.PREVIEW, PREVIEW_SIZE],
125  [IconType.LONGITUDINAL, LONGITUDINAL_SIZE],
126  [IconType.VERTICAL, VERTICAL_SIZE]
127])
128
129@Component
130struct ContentItemStruct {
131  @Prop @Watch('onPropChange') iconStyle: IconType | null = null;
132  @Prop @Watch('onPropChange') icon: ResourceStr | null = null;
133  @Prop @Watch('onPropChange') primaryText: ResourceStr | null = null;
134  @Prop @Watch('onPropChange') secondaryText: ResourceStr | null = null;
135  @Prop @Watch('onPropChange') description: ResourceStr | null = null;
136  @State itemRowSpace: number = NORMAL_ITEM_ROW_SPACE;
137  @Prop leftWidth: string = LEFT_PART_WIDTH;
138  @State primaryTextColor: ResourceColor = $r('sys.color.ohos_id_color_text_primary');
139  @State secondaryTextColor: ResourceColor = $r('sys.color.ohos_id_color_text_secondary');
140  @State descriptionColor: ResourceColor = $r('sys.color.ohos_id_color_text_secondary');
141  @Prop fontSizeScale: number;
142  @Prop parentDirection: FlexDirection;
143  @Prop itemDirection: FlexDirection;
144
145  onWillApplyTheme(theme: Theme): void {
146    this.primaryTextColor = theme.colors.fontPrimary;
147    this.secondaryTextColor = theme.colors.fontSecondary;
148    this.descriptionColor = theme.colors.fontTertiary;
149  }
150
151  onPropChange() {
152    if (this.icon == null && this.iconStyle == null) {
153      this.itemRowSpace = SPECIAL_ITEM_ROW_SPACE;
154    } else {
155      this.itemRowSpace = NORMAL_ITEM_ROW_SPACE;
156    }
157  }
158
159  aboutToAppear() {
160    this.onPropChange();
161  }
162
163  @Builder
164  createIcon() {
165    if (this.icon != null && this.iconStyle != null && ICON_SIZE_MAP.has(this.iconStyle)) {
166      if (this.iconStyle <= IconType.PREVIEW) {
167        Image(this.icon)
168          .objectFit(ImageFit.Contain)
169          .width(ICON_SIZE_MAP.get(this.iconStyle))
170          .height(ICON_SIZE_MAP.get(this.iconStyle))
171          .borderRadius($r('sys.float.ohos_id_corner_radius_default_m'))
172          .focusable(false)
173          .draggable(false)
174          .fillColor($r('sys.color.ohos_id_color_secondary'))
175          .flexShrink(0)
176      } else {
177        Image(this.icon)
178          .objectFit(ImageFit.Contain)
179          .constraintSize({
180            minWidth: SPECIAL_ICON_SIZE,
181            maxWidth: ICON_SIZE_MAP.get(this.iconStyle),
182            minHeight: SPECIAL_ICON_SIZE,
183            maxHeight: ICON_SIZE_MAP.get(this.iconStyle)
184          })
185          .borderRadius($r('sys.float.ohos_id_corner_radius_default_m'))
186          .focusable(false)
187          .draggable(false)
188          .fillColor($r('sys.color.ohos_id_color_secondary'))
189          .flexShrink(0)
190      }
191    }
192  }
193
194  @Builder
195  createText() {
196    Column({ space: TEXT_COLUMN_SPACE }) {
197      Text(this.primaryText)
198        .fontSize($r('sys.float.ohos_id_text_size_body1'))
199        .fontColor(this.primaryTextColor)
200        .textOverflow({ overflow: TextOverflow.Ellipsis })
201        .fontWeight(FontWeight.Medium)
202        .focusable(true)
203        .draggable(false)
204      if (this.secondaryText != null) {
205        Text(this.secondaryText)
206          .fontSize($r('sys.float.ohos_id_text_size_body2'))
207          .fontColor(this.secondaryTextColor)
208          .textOverflow({ overflow: TextOverflow.Ellipsis })
209          .draggable(false)
210      }
211      if (this.description != null) {
212        Text(this.description)
213          .fontSize($r('sys.float.ohos_id_text_size_body2'))
214          .fontColor(this.descriptionColor)
215          .textOverflow({ overflow: TextOverflow.Ellipsis })
216          .draggable(false)
217      }
218    }
219    .flexShrink(1)
220    .margin(this.fontSizeScale >= FontSizeScaleLevel.LEVEL1 ? undefined : {
221      top: TEXT_SAFE_MARGIN,
222      bottom: TEXT_SAFE_MARGIN
223    })
224    .alignItems(HorizontalAlign.Start)
225  }
226
227  isColumnDirection() {
228    return this.itemDirection === FlexDirection.Column;
229  }
230
231  isParentColumnDirection() {
232    return this.parentDirection === FlexDirection.Column;
233  }
234
235  getItemSpace() {
236    if (this.isColumnDirection()) {
237      return LengthMetrics.resource($r('sys.float.padding_level1'));
238    }
239    return LengthMetrics.vp(this.itemRowSpace);
240  }
241
242  build() {
243    Flex({
244      space: { main: this.getItemSpace() },
245      direction: this.itemDirection,
246      justifyContent: FlexAlign.Start,
247      alignItems: this.isColumnDirection() ? ItemAlign.Start : ItemAlign.Center,
248    }) {
249      this.createIcon()
250      this.createText()
251    }
252    .margin({
253      end: this.isParentColumnDirection() ?
254      LengthMetrics.vp(0) :
255      LengthMetrics.vp(16)
256    })
257    .padding({ start: LengthMetrics.vp(LISTITEM_PADDING) })
258    .flexShrink(this.isParentColumnDirection() ? 0 : 1)
259  }
260}
261
262class CreateIconParam {
263  public icon?: OperateIcon;
264}
265
266@Component
267struct OperateItemStruct {
268  @Prop @Watch('onPropChange') arrow: OperateIcon | null = null;
269  @Prop @Watch('onPropChange') icon: OperateIcon | null = null;
270  @Prop @Watch('onPropChange') subIcon: OperateIcon | null = null;
271  @Prop @Watch('onPropChange') button: OperateButton | null = null;
272  @Prop @Watch('onPropChange') switch: OperateCheck | null = null;
273  @Prop @Watch('onPropChange') checkBox: OperateCheck | null = null;
274  @Prop @Watch('onPropChange') radio: OperateCheck | null = null;
275  @Prop @Watch('onPropChange') image: ResourceStr | null = null;
276  @Prop @Watch('onPropChange') text: ResourceStr | null = null;
277  @State switchState: boolean = false
278  @State radioState: boolean = false
279  @State checkBoxState: boolean = false
280  @Prop rightWidth: string = RIGHT_PART_WIDTH;
281  @State secondaryTextColor: ResourceColor = $r('sys.color.ohos_id_color_text_secondary');
282  @State hoveringColor: ResourceColor = '#0d000000';
283  @State activedColor: ResourceColor = '#1a0a59f7';
284  @Link parentCanFocus: boolean
285  @Link parentCanTouch: boolean
286  @Link parentIsHover: boolean
287  @Link parentCanHover: boolean
288  @Link parentIsActive: boolean
289  @Link parentFrontColor: ResourceColor;
290  @Link parentDirection: FlexDirection;
291  @State rowSpace: number = DEFAULT_ROW_SPACE;
292
293  onWillApplyTheme(theme: Theme): void {
294    this.secondaryTextColor = theme.colors.fontSecondary;
295    this.hoveringColor = theme.colors.interactiveHover;
296    this.activedColor = theme.colors.interactiveActive;
297  }
298
299  onPropChange() {
300    if (this.switch != null) {
301      this.switchState = this.switch.isCheck as boolean;
302    }
303    if (this.radio != null) {
304      this.radioState = this.radio.isCheck as boolean;
305    }
306    if (this.checkBox != null) {
307      this.checkBoxState = this.checkBox.isCheck as boolean;
308    }
309
310    if ((this.button == null && this.image == null && this.icon != null && this.text != null) ||
311      (this.button == null && this.image == null && this.icon == null && this.arrow != null && this.text != null)) {
312      this.rowSpace = SPECICAL_ROW_SPACE;
313    } else {
314      this.rowSpace = DEFAULT_ROW_SPACE;
315    }
316  }
317
318  aboutToAppear() {
319    this.onPropChange();
320  }
321
322  @Builder
323  createButton() {
324    Button() {
325      Row() {
326        Text(this.button?.text as ResourceStr)
327          .focusable(true)
328      }
329      .padding({
330        left: TEXT_SAFE_MARGIN,
331        right: TEXT_SAFE_MARGIN
332      })
333    }
334    .padding({ top: 0, bottom: 0 })
335    .margin({ end: LengthMetrics.vp(LISTITEM_PADDING) })
336    .hitTestBehavior(HitTestMode.Block)
337    .fontSize($r('sys.float.ohos_id_text_size_button3'))
338    .fontColor($r('sys.color.ohos_id_color_text_primary_activated_transparent'))
339    .constraintSize({
340      minHeight: BUTTON_SIZE
341    })
342    .backgroundColor($r('sys.color.ohos_id_color_button_normal'))
343    .labelStyle({
344      maxLines: TEXT_MAX_LINE
345    })
346    .onFocus(() => {
347      this.parentCanFocus = false;
348    })
349    .onHover((isHover: boolean) => {
350      this.parentCanHover = false
351      if (isHover && this.parentFrontColor === this.hoveringColor) {
352        this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
353      }
354      if (!isHover) {
355        this.parentCanHover = true
356        if (this.parentIsHover) {
357          this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
358            (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
359        }
360      }
361    })
362  }
363
364  @Builder
365  createIcon(param: CreateIconParam) {
366    Button({ type: ButtonType.Normal }) {
367      Image(param.icon?.value)
368        .height(OPERATEITEM_ICONLIKE_SIZE)
369        .width(OPERATEITEM_ICONLIKE_SIZE)
370        .focusable(true)
371        .fillColor($r('sys.color.ohos_id_color_primary'))
372        .draggable(false)
373    }
374    .hitTestBehavior(HitTestMode.Block)
375    .backgroundColor(Color.Transparent)
376    .height(OPERATEITEM_ICON_CLICKABLE_SIZE)
377    .width(OPERATEITEM_ICON_CLICKABLE_SIZE)
378    .borderRadius($r('sys.float.ohos_id_corner_radius_clicked'))
379    .onFocus(() => {
380      this.parentCanFocus = false;
381    })
382    .onHover((isHover: boolean) => {
383      this.parentCanHover = false
384      if (isHover && this.parentFrontColor === this.hoveringColor) {
385        this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
386      }
387      if (!isHover) {
388        this.parentCanHover = true
389        if (this.parentIsHover) {
390          this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
391            (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
392        }
393      }
394    })
395    .onClick(param.icon?.action)
396    .flexShrink(0)
397  }
398
399  @Builder
400  createImage() {
401    Image(this.image)
402      .height(OPERATEITEM_IMAGE_SIZE)
403      .width(OPERATEITEM_IMAGE_SIZE)
404      .draggable(false)
405      .margin({ end: LengthMetrics.vp(LISTITEM_PADDING) })
406  }
407
408  @Builder
409  createText() {
410    Text(this.text)
411      .margin({ end: LengthMetrics.vp(LISTITEM_PADDING) })
412      .fontSize($r('sys.float.ohos_id_text_size_body2'))
413      .fontColor(this.secondaryTextColor)
414      .draggable(false)
415      .flexShrink(1)
416  }
417
418  @Builder
419  createArrow() {
420    Button({ type: ButtonType.Normal }) {
421      Image(this.arrow?.value)
422        .height(OPERATEITEM_ICONLIKE_SIZE)
423        .width(OPERATEITEM_ARROW_WIDTH)
424        .focusable(true)
425        .fillColor($r('sys.color.ohos_id_color_fourth'))
426        .draggable(false)
427        .matchTextDirection(true)
428    }
429    .margin({ end: LengthMetrics.vp(LISTITEM_PADDING) })
430    .hitTestBehavior(this.arrow?.action !== undefined ? HitTestMode.Block : HitTestMode.Transparent)
431    .backgroundColor(Color.Transparent)
432    .height(OPERATEITEM_ICONLIKE_SIZE)
433    .width(OPERATEITEM_ARROW_WIDTH)
434    .onFocus(() => {
435      this.parentCanFocus = false;
436    })
437    .stateEffect(this.arrow?.action !== undefined)
438    .hoverEffect(this.arrow?.action !== undefined ? HoverEffect.Auto : HoverEffect.None)
439    .onHover((isHover: boolean) => {
440      if (this.arrow?.action === undefined) {
441        return;
442      }
443      this.parentCanHover = false
444      if (isHover) {
445        this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
446      }
447      if (!isHover) {
448        this.parentCanHover = true
449        if (this.parentIsHover) {
450          this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
451            (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
452        }
453      }
454    })
455    .onClick(this.arrow?.action)
456  }
457
458  @Builder
459  createRadio() {
460    Radio({ value: '', group: '' })
461      .margin({ end: LengthMetrics.vp(LISTITEM_PADDING) })
462      .checked(this.radioState)
463      .onChange(this.radio?.onChange)
464      .height(OPERATEITEM_ICONLIKE_SIZE)
465      .width(OPERATEITEM_ICONLIKE_SIZE)
466      .padding(OPERATEITEM_SELECTIONBOX_PADDING_SIZE)
467      .onFocus(() => {
468        this.parentCanFocus = false;
469      })
470      .hitTestBehavior(HitTestMode.Block)
471      .flexShrink(0)
472      .onHover((isHover: boolean) => {
473        this.parentCanHover = false
474        if (isHover && this.parentFrontColor === this.hoveringColor) {
475          this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
476        }
477        if (!isHover) {
478          this.parentCanHover = true
479          if (this.parentIsHover) {
480            this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
481              (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
482          }
483        }
484      })
485  }
486
487  @Builder
488  createCheckBox() {
489    Checkbox()
490      .margin({ end: LengthMetrics.vp(LISTITEM_PADDING) })
491      .select(this.checkBoxState)
492      .onChange(this.checkBox?.onChange)
493      .height(OPERATEITEM_ICONLIKE_SIZE)
494      .width(OPERATEITEM_ICONLIKE_SIZE)
495      .padding(OPERATEITEM_SELECTIONBOX_PADDING_SIZE)
496      .onFocus(() => {
497        this.parentCanFocus = false;
498      })
499      .hitTestBehavior(HitTestMode.Block)
500      .onHover((isHover: boolean) => {
501        this.parentCanHover = false
502        if (isHover && this.parentFrontColor === this.hoveringColor) {
503          this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
504        }
505        if (!isHover) {
506          this.parentCanHover = true
507          if (this.parentIsHover) {
508            this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
509              (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
510          }
511        }
512      })
513  }
514
515  @Builder
516  createSwitch() {
517    Row() {
518      Toggle({ type: ToggleType.Switch, isOn: this.switchState })
519        .onChange(this.switch?.onChange)
520        .onClick(() => {
521          this.switchState = !this.switchState
522        })
523        .hitTestBehavior(HitTestMode.Block)
524    }
525    .margin({ end: LengthMetrics.vp(SWITCH_PADDING) })
526    .height(OPERATEITEM_ICON_CLICKABLE_SIZE)
527    .width(OPERATEITEM_ICON_CLICKABLE_SIZE)
528    .justifyContent(FlexAlign.Center)
529    .onFocus(() => {
530      this.parentCanFocus = false;
531    })
532    .onHover((isHover: boolean) => {
533      this.parentCanHover = false
534      if (isHover && this.parentFrontColor === this.hoveringColor) {
535        this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
536      }
537      if (!isHover) {
538        this.parentCanHover = true
539        if (this.parentIsHover) {
540          this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
541            (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
542        }
543      }
544    })
545  }
546
547  @Builder
548  createTextArrow() {
549    Button({ type: ButtonType.Normal }) {
550      if (this.parentDirection === FlexDirection.Column) {
551        Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
552          Text(this.text)
553            .fontSize($r('sys.float.ohos_id_text_size_body2'))
554            .fontColor(this.secondaryTextColor)
555            .focusable(true)
556            .draggable(false)
557            .constraintSize({
558              maxWidth: `calc(100% - ${OPERATEITEM_ARROW_WIDTH}vp)`
559            })
560          Image(this.arrow?.value)
561            .height(OPERATEITEM_ICONLIKE_SIZE)
562            .width(OPERATEITEM_ARROW_WIDTH)
563            .fillColor($r('sys.color.ohos_id_color_fourth'))
564            .focusable(false)
565            .draggable(false)
566            .matchTextDirection(true)
567        }
568        .padding({
569          start: LengthMetrics.vp(TEXT_SAFE_MARGIN),
570          end: LengthMetrics.vp(LISTITEM_PADDING)
571        })
572      } else {
573        Row({ space: SPECICAL_ROW_SPACE }) {
574          Text(this.text)
575            .fontSize($r('sys.float.ohos_id_text_size_body2'))
576            .fontColor(this.secondaryTextColor)
577            .focusable(true)
578            .draggable(false)
579            .constraintSize({
580              maxWidth: `calc(100% - ${OPERATEITEM_ARROW_WIDTH}vp)`
581            })
582          Image(this.arrow?.value)
583            .height(OPERATEITEM_ICONLIKE_SIZE)
584            .width(OPERATEITEM_ARROW_WIDTH)
585            .fillColor($r('sys.color.ohos_id_color_fourth'))
586            .focusable(false)
587            .draggable(false)
588            .matchTextDirection(true)
589        }
590        .padding({
591          start: LengthMetrics.vp(TEXT_SAFE_MARGIN),
592          end: LengthMetrics.vp(LISTITEM_PADDING)
593        })
594      }
595    }
596    .hitTestBehavior(this.arrow?.action !== undefined ? HitTestMode.Block : HitTestMode.Transparent)
597    .labelStyle({
598      maxLines: TEXT_MAX_LINE
599    })
600    .backgroundColor(Color.Transparent)
601    .constraintSize({ minHeight: TEXT_ARROW_HEIGHT })
602    .borderRadius($r('sys.float.ohos_id_corner_radius_clicked'))
603    .onFocus(() => {
604      this.parentCanFocus = false;
605    })
606    .padding({
607      top: 0,
608      bottom: 0,
609      left: 0,
610      right: 0
611    })
612    .stateEffect(this.arrow?.action !== undefined)
613    .hoverEffect(this.arrow?.action !== undefined ? HoverEffect.Auto : HoverEffect.None)
614    .onHover((isHover: boolean) => {
615      if (this.arrow?.action === undefined) {
616        return;
617      }
618      this.parentCanHover = false
619      if (isHover) {
620        this.parentFrontColor = this.parentIsActive ? this.activedColor : Color.Transparent.toString();
621      }
622      if (!isHover) {
623        this.parentCanHover = true
624        if (this.parentIsHover) {
625          this.parentFrontColor = this.parentIsHover ? this.hoveringColor :
626            (this.parentIsActive ? this.activedColor : Color.Transparent.toString());
627        }
628      }
629    })
630    .onClick(this.arrow?.action)
631  }
632
633  getFlexOptions(): FlexOptions {
634    let flexOptions: FlexOptions = { alignItems: ItemAlign.Center };
635    if (this.parentDirection === FlexDirection.Column) {
636      flexOptions.justifyContent = FlexAlign.SpaceBetween;
637    } else {
638      flexOptions.space = { main: LengthMetrics.vp(this.rowSpace) };
639      flexOptions.justifyContent = FlexAlign.End;
640    }
641    return flexOptions;
642  }
643
644  build() {
645    Flex(this.getFlexOptions()) {
646      if (this.button != null) {
647        this.createButton();
648      } else if (this.image != null) {
649        this.createImage()
650      } else if (this.icon != null && this.text != null) {
651        this.createText()
652        this.createIcon({ icon: this.icon })
653      } else if (this.arrow != null && this.text == null) {
654        this.createArrow()
655      } else if (this.arrow != null && this.text != null) {
656        this.createTextArrow();
657      } else if (this.text != null) {
658        this.createText()
659      } else if (this.radio != null) {
660        this.createRadio()
661      } else if (this.checkBox != null) {
662        this.createCheckBox()
663      } else if (this.switch != null) {
664        this.createSwitch()
665      } else if (this.icon != null) {
666        this.createIcon({ icon: this.icon })
667        if (this.subIcon != null) {
668          this.createIcon({ icon: this.subIcon });
669        }
670      }
671    }
672    .width(this.parentDirection === FlexDirection.Column ? undefined : this.rightWidth)
673  }
674}
675
676@Component
677export struct ComposeListItem {
678  @Prop @Watch('onPropChange') contentItem: ContentItem | null = null;
679  @Prop @Watch('onPropChange') operateItem: OperateItem | null = null;
680  @State frontColor: ResourceColor = Color.Transparent.toString();
681  @State borderSize: number = 0;
682  @State canFocus: boolean = false
683  @State canTouch: boolean = true
684  @State canHover: boolean = true
685  @State isHover: boolean = true
686  @State itemHeight: number = ItemHeight.FIRST_HEIGHT;
687  @State isActive: boolean = false
688  @State hoveringColor: ResourceColor = '#0d000000';
689  @State touchDownColor: ResourceColor = '#1a000000';
690  @State activedColor: ResourceColor = '#1a0a59f7';
691  @State focusOutlineColor: ResourceColor = $r('sys.color.ohos_id_color_focused_outline');
692  @State @Watch('onFontSizeScaleChange') fontSizeScale: number = 1;
693  @State containerDirection: FlexDirection = FlexDirection.Row;
694  @State contentItemDirection: FlexDirection = FlexDirection.Row;
695  @State containerPadding?: Padding = undefined;
696  @State textArrowLeftSafeOffset: number = 0;
697  private isFollowingSystemFontScale = this.getUIContext().isFollowingSystemFontScale();
698  private maxFontScale = this.getUIContext().getMaxFontScale();
699
700  onWillApplyTheme(theme: Theme): void {
701    this.hoveringColor = theme.colors.interactiveHover;
702    this.touchDownColor = theme.colors.interactivePressed;
703    this.activedColor = theme.colors.interactiveActive;
704    this.focusOutlineColor = theme.colors.interactiveFocus;
705  }
706
707  onPropChange() {
708    if (this.contentItem === undefined) {
709      if (this.operateItem?.image !== undefined ||
710        this.operateItem?.icon !== undefined ||
711        this.operateItem?.subIcon !== undefined) {
712        this.itemHeight = OPERATEITEM_IMAGE_SIZE + SAFE_LIST_PADDING
713      }
714      return
715    }
716
717    if (this.contentItem?.secondaryText === undefined && this.contentItem?.description === undefined) {
718      if (this.contentItem?.icon === undefined) {
719        this.itemHeight = ItemHeight.FIRST_HEIGHT
720      } else {
721        this.itemHeight = this.contentItem.iconStyle as number <= IconType.HEAD_SCULPTURE ?
722        ItemHeight.SECOND_HEIGHT : ItemHeight.THIRD_HEIGHT;
723      }
724    } else if (this.contentItem.description === undefined) {
725      if (this.contentItem.icon === undefined ||
726        (this.contentItem.icon !== undefined && this.contentItem.iconStyle as number <= IconType.SYSTEM_ICON)) {
727        this.itemHeight = ItemHeight.THIRD_HEIGHT
728      } else {
729        this.itemHeight = ItemHeight.FOURTH_HEIGHT
730      }
731    } else {
732      this.itemHeight = ItemHeight.FIFTH_HEIGHT
733    }
734
735    if (ICON_SIZE_MAP.get(this.contentItem?.iconStyle as number) as number >= this.itemHeight) {
736      this.itemHeight = ICON_SIZE_MAP.get(this.contentItem?.iconStyle as number) as number + SAFE_LIST_PADDING;
737    }
738
739  }
740
741  aboutToAppear() {
742    this.onPropChange();
743  }
744
745  calculatedRightWidth(): string {
746    if (this.operateItem === null || JSON.stringify(this.operateItem) === '{}') {
747      return RIGHT_CONTENT_NULL_RIGHTWIDTH;
748    } else if (this.operateItem?.switch != null && this.operateItem?.text == null) {
749      return RIGHT_ONLY_SWITCH_WIDTH;
750    } else if (this.operateItem?.checkbox != null && this.operateItem?.text == null) {
751      return RIGHT_ONLY_CHECKBOX_WIDTH;
752    } else if (this.operateItem?.radio != null && this.operateItem?.text == null) {
753      return RIGHT_ONLY_RADIO_WIDTH;
754    } else if (this.operateItem?.icon != null && this.operateItem?.text == null) {
755      if (this.operateItem?.subIcon != null) {
756        return RIGHT_ICON_SUB_ICON_WIDTH;
757      }
758      return RIGHT_ONLY_ICON_WIDTH;
759    } else if (this.operateItem?.image != null && this.operateItem?.text == null) {
760      return RIGHT_ONLY_IMAGE_WIDTH;
761    } else if (this.operateItem?.arrow != null && this.operateItem?.text == null) {
762      return RIGHT_ONLY_ARROW_WIDTH;
763    } else {
764      return RIGHT_PART_WIDTH;
765    }
766  }
767
768  decideContentItemDirection(): FlexDirection {
769    if (this.fontSizeScale >= FontSizeScaleLevel.LEVEL1 &&
770      this.contentItem?.iconStyle && this.contentItem?.iconStyle > IconType.HEAD_SCULPTURE) {
771      return FlexDirection.Column;
772    }
773    return FlexDirection.Row;
774  }
775
776  decideContainerDirection(): FlexDirection {
777    if (this.fontSizeScale < FontSizeScaleLevel.LEVEL1 || !this.contentItem) {
778      return FlexDirection.Row;
779    }
780    if (this.operateItem?.button) {
781      return FlexDirection.Column;
782    } else if (this.operateItem?.image) {
783      return FlexDirection.Row;
784    } else if (this.operateItem?.icon && this.operateItem?.text) {
785      return FlexDirection.Column;
786    } else if (this.operateItem?.arrow) {
787      if (!this.operateItem?.text) {
788        return FlexDirection.Row;
789      }
790      this.textArrowLeftSafeOffset = TEXT_SAFE_MARGIN;
791      return FlexDirection.Column;
792    } else if (this.operateItem?.text) {
793      return FlexDirection.Column;
794    } else {
795      return FlexDirection.Row;
796    }
797  }
798
799  onFontSizeScaleChange() {
800    this.containerDirection = this.decideContainerDirection();
801    this.contentItemDirection = this.decideContentItemDirection();
802    if (this.fontSizeScale >= FontSizeScaleLevel.LEVEL3) {
803      this.containerPadding = {
804        top: $r('sys.float.padding_level12'),
805        bottom: $r('sys.float.padding_level12'),
806      };
807    } else if (this.fontSizeScale >= FontSizeScaleLevel.LEVEL2) {
808      this.containerPadding = {
809        top: $r('sys.float.padding_level10'),
810        bottom: $r('sys.float.padding_level10'),
811      };
812    } else if (this.fontSizeScale >= FontSizeScaleLevel.LEVEL1) {
813      this.containerPadding = {
814        top: $r('sys.float.padding_level8'),
815        bottom: $r('sys.float.padding_level8'),
816      };
817    } else {
818      this.containerPadding = undefined;
819    }
820  }
821
822  isSingleLine(): boolean {
823    return !this.contentItem?.secondaryText && !this.contentItem?.description;
824  }
825
826  getOperateOffset(): LengthMetrics {
827    if (this.containerDirection === FlexDirection.Row) {
828      return LengthMetrics.vp(0);
829    }
830    let iconSize = ICON_SIZE_MAP.get(this.contentItem?.iconStyle as number);
831    if (this.contentItem?.icon && iconSize && iconSize <= HEADSCULPTURE_SIZE) {
832      return LengthMetrics.vp(iconSize + NORMAL_ITEM_ROW_SPACE + LISTITEM_PADDING - this.textArrowLeftSafeOffset);
833    }
834    return LengthMetrics.vp(LISTITEM_PADDING - this.textArrowLeftSafeOffset);
835  }
836
837  getMainSpace(): LengthMetrics {
838    if (this.containerDirection === FlexDirection.Column) {
839      return LengthMetrics.resource(this.isSingleLine() ? $r('sys.float.padding_level1') : $r('sys.float.padding_level8'));
840    }
841    return LengthMetrics.vp(0);
842  }
843
844  getFlexOptions(): FlexOptions {
845    if (this.containerDirection === FlexDirection.Column) {
846      return {
847        space: { main: this.getMainSpace() },
848        justifyContent: FlexAlign.Center,
849        alignItems: ItemAlign.Start,
850        direction: this.containerDirection,
851      };
852    }
853    return {
854      justifyContent: FlexAlign.SpaceBetween,
855      alignItems: ItemAlign.Center,
856      direction: this.containerDirection,
857    };
858  }
859
860  decideFontSizeScale(): number {
861    if (!this.isFollowingSystemFontScale) {
862      return 1;
863    }
864    return Math.min(
865      this.maxFontScale,
866      (this.getUIContext().getHostContext() as common.UIAbilityContext)?.config.fontSizeScale ?? 1
867    )
868  }
869
870  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult {
871    this.fontSizeScale = this.decideFontSizeScale();
872    let sizeResult: SizeResult = { height: 0, width: 0 };
873    children.forEach((child) => {
874      let childMeasureResult: MeasureResult = child.measure(constraint);
875      sizeResult.width = childMeasureResult.width;
876      sizeResult.height = childMeasureResult.height;
877    });
878    return sizeResult;
879  }
880
881  build() {
882    Stack() {
883      Flex(this.getFlexOptions()) {
884        if (this.contentItem === null) {
885          ContentItemStruct({})
886        }
887        if (this.contentItem !== null) {
888          ContentItemStruct({
889            icon: this.contentItem?.icon,
890            iconStyle: this.contentItem?.iconStyle,
891            primaryText: this.contentItem?.primaryText,
892            secondaryText: this.contentItem?.secondaryText,
893            description: this.contentItem?.description,
894            fontSizeScale: this.fontSizeScale,
895            parentDirection: this.containerDirection,
896            itemDirection: this.contentItemDirection,
897          })
898        }
899        if (this.operateItem !== null) {
900          OperateItemStruct({
901            icon: this.operateItem?.icon,
902            subIcon: this.operateItem?.subIcon,
903            button: this.operateItem?.button,
904            switch: this.operateItem?.switch,
905            checkBox: this.operateItem?.checkbox,
906            radio: this.operateItem?.radio,
907            image: this.operateItem?.image,
908            text: this.operateItem?.text,
909            arrow: this.operateItem?.arrow,
910            parentCanFocus: this.canFocus,
911            parentCanTouch: this.canTouch,
912            parentIsHover: this.isHover,
913            parentFrontColor: this.frontColor,
914            parentIsActive: this.isActive,
915            parentCanHover: this.canHover,
916            rightWidth: this.calculatedRightWidth(),
917            parentDirection: this.containerDirection,
918          })
919            .flexShrink(0)
920            .onFocus(() => {
921              this.canFocus = false
922            })
923            .onBlur(() => {
924              this.canFocus = true
925            }).padding({start: this.getOperateOffset()});
926        }
927      }
928      .constraintSize({
929        minHeight: this.itemHeight
930      })
931      .focusable(true)
932      .borderRadius($r('sys.float.ohos_id_corner_radius_default_m'))
933      .backgroundColor(this.frontColor)
934      .onFocus(() => {
935        this.canFocus = true
936      })
937      .onBlur(() => {
938        this.canFocus = false
939      })
940      .onHover((isHover: boolean) => {
941        this.isHover = isHover
942        if (this.canHover) {
943          this.frontColor = isHover ? this.hoveringColor :
944            (this.isActive ? this.activedColor : Color.Transparent.toString());
945        }
946      })
947      .stateStyles({
948        focused: {
949          .border({
950            radius: $r('sys.float.ohos_id_corner_radius_default_m'),
951            width: ITEM_BORDER_SHOWN,
952            color: this.focusOutlineColor,
953            style: BorderStyle.Solid
954          })
955        },
956        normal: {
957          .border({
958            radius: $r('sys.float.ohos_id_corner_radius_default_m'),
959            width: ITEM_BORDER_SHOWN,
960            color: Color.Transparent
961          })
962        },
963        pressed: {
964          .backgroundColor(this.touchDownColor)
965        }
966      }).padding(this.containerPadding)
967    }
968    .width('100%')
969    .padding({
970      left: STACK_PADDING,
971      right: STACK_PADDING
972    })
973  }
974}