1/*
2 * Copyright (c) 2023-2023 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
16export enum IconType {
17  BADGE = 1,
18  NORMAL_ICON,
19  SYSTEM_ICON,
20  HEAD_SCULPTURE,
21  APP_ICON,
22  PREVIEW,
23  LONGITUDINAL,
24  VERTICAL
25}
26
27enum ItemHeight {
28  FIRST_HEIGHT = 48,
29  SECOND_HEIGHT = 56,
30  THIRD_HEIGHT = 64,
31  FOURTH_HEIGHT = 72,
32  FIFTH_HEIGHT = 96
33}
34
35export type OperateItem = {
36  icon?: OperateIcon,
37  subIcon ?: OperateIcon,
38  button?: OperateButton;
39  switch?: OperateCheck;
40  checkbox?: OperateCheck;
41  radio?: OperateCheck;
42  image?: ResourceStr;
43  text?: ResourceStr;
44  arrow?: OperateIcon;
45}
46
47export type ContentItem = {
48  iconStyle?: IconType;
49  icon?: ResourceStr;
50  primaryText?: ResourceStr;
51  secondaryText?: ResourceStr;
52  description?: ResourceStr;
53}
54
55const LISTITEMCARD_BORDER_HIDDEN = 0;
56const TEXT_MAX_LINE = 1;
57const LISTITEMCARD_BORDER_SHOWN = 2;
58const TEXT_COLUMN_SPACE = 2;
59const TEXT_SAFE_MARGIN = 8;
60const BADGE_SIZE = 8;
61const SMALL_ICON_SIZE = 16;
62const SYSTEM_ICON_SIZE = 24;
63const SAFE_LIST_PADDING = 32;
64const HEADSCULPTURE_SIZE = 40;
65const BUTTON_SIZE = 28;
66const APP_ICON_SIZE = 64;
67const PREVIEW_SIZE = 96;
68const LONGITUDINAL_SIZE = 96;
69const VERTICAL_SIZE = 96;
70const NORMAL_ITEM_ROW_SPACE = 16;
71const SPECIAL_ITEM_ROW_SPACE = 0;
72const SPECIAL_ICON_SIZE = 0;
73const DEFAULT_ROW_SPACE = 0;
74const SPECICAL_ROW_SPACE = 4;
75const OPERATEITEM_ICONLIKE_SIZE = 24;
76const OPERATEITEM_ARROW_WIDTH = 12
77const OPERATEITEM_ICON_CLICKABLE_SIZE = 48;
78const OPERATEITEM_IMAGE_SIZE = 48;
79const HOVERING_COLOR = '#0d000000';
80const TOUCH_DOWN_COLOR = '#1a000000';
81const ACTIVED_COLOR = '#1a0a59f7';
82
83@Component
84struct ContentItemStruct {
85  iconStyle: IconType = null
86  icon: Resource = null
87  title: string = null
88  subtitle: string = null
89  description: string = null
90  private iconSizeMap: Map<number, number> = new Map([
91    [IconType.BADGE, BADGE_SIZE],
92    [IconType.NORMAL_ICON, SMALL_ICON_SIZE],
93    [IconType.SYSTEM_ICON, SYSTEM_ICON_SIZE],
94    [IconType.HEAD_SCULPTURE, HEADSCULPTURE_SIZE],
95    [IconType.APP_ICON, APP_ICON_SIZE],
96    [IconType.PREVIEW, PREVIEW_SIZE],
97    [IconType.LONGITUDINAL, LONGITUDINAL_SIZE],
98    [IconType.VERTICAL, VERTICAL_SIZE]
99  ])
100  private itemHeight: number = ItemHeight.FIRST_HEIGHT
101  private itemRowSpace: number = NORMAL_ITEM_ROW_SPACE
102
103  aboutToAppear() {
104    if (this.subtitle == null && this.description == null) {
105      if (this.icon == null) {
106        this.itemHeight = ItemHeight.FIRST_HEIGHT
107      }
108      else {
109        this.itemHeight = this.iconStyle <= IconType.HEAD_SCULPTURE ? ItemHeight.SECOND_HEIGHT : ItemHeight.THIRD_HEIGHT
110      }
111    }
112    else if (this.description == null) {
113      if (this.icon == null || (this.icon != null && this.iconStyle <= IconType.SYSTEM_ICON)) {
114        this.itemHeight = ItemHeight.THIRD_HEIGHT
115      }
116      else {
117        this.itemHeight = ItemHeight.FOURTH_HEIGHT
118      }
119    }
120    else {
121      this.itemHeight = ItemHeight.FIFTH_HEIGHT
122    }
123
124    if (this.icon == null && this.iconStyle == null) {
125      this.itemRowSpace = SPECIAL_ITEM_ROW_SPACE
126    }
127
128    if (this.iconSizeMap.get(this.iconStyle) >= this.itemHeight) {
129      this.itemHeight = this.iconSizeMap.get(this.iconStyle) + SAFE_LIST_PADDING;
130    }
131  }
132
133  @Builder
134  createIcon() {
135    if (this.icon != null && this.iconStyle != null) {
136      if (this.iconStyle <= IconType.PREVIEW) {
137        Image(this.icon)
138          .objectFit(ImageFit.Contain)
139          .width(this.iconSizeMap.get(this.iconStyle))
140          .height(this.iconSizeMap.get(this.iconStyle))
141          .borderRadius($r('sys.float.ohos_id_corner_radius_default_m'))
142          .focusable(true)
143      }
144      else {
145        Image(this.icon)
146          .objectFit(ImageFit.Contain)
147          .constraintSize({
148            minWidth: SPECIAL_ICON_SIZE,
149            maxWidth: this.iconSizeMap.get(this.iconStyle),
150            minHeight: SPECIAL_ICON_SIZE,
151            maxHeight: this.iconSizeMap.get(this.iconStyle)
152          })
153          .borderRadius($r('sys.float.ohos_id_corner_radius_default_m'))
154          .focusable(true)
155      }
156    }
157  }
158
159  @Builder
160  createText() {
161    Column({ space: TEXT_COLUMN_SPACE }) {
162      Text(this.title)
163        .fontSize($r('sys.float.ohos_id_text_size_body1'))
164        .fontColor($r('sys.color.ohos_id_color_text_primary'))
165        .maxLines(TEXT_MAX_LINE)
166        .textOverflow({ overflow: TextOverflow.Ellipsis })
167        .focusable(true)
168      if (this.subtitle != null) {
169        Text(this.subtitle)
170          .fontSize($r('sys.float.ohos_id_text_size_body2'))
171          .fontColor($r('sys.color.ohos_id_color_text_secondary'))
172          .maxLines(TEXT_MAX_LINE)
173          .textOverflow({ overflow: TextOverflow.Ellipsis })
174          .focusable(true)
175      }
176      if (this.description != null) {
177        Text(this.description)
178          .fontSize($r('sys.float.ohos_id_text_size_body2'))
179          .fontColor($r('sys.color.ohos_id_color_text_secondary'))
180          .maxLines(TEXT_MAX_LINE)
181          .textOverflow({ overflow: TextOverflow.Ellipsis })
182          .focusable(true)
183      }
184    }
185    .margin({
186      top: TEXT_SAFE_MARGIN,
187      bottom: TEXT_SAFE_MARGIN
188    })
189    .alignItems(HorizontalAlign.Start)
190  }
191
192  build() {
193    Row({ space: this.itemRowSpace }) {
194      this.createIcon()
195      this.createText()
196    }
197    .height(this.itemHeight)
198  }
199}
200
201@Component
202struct OperateItemStruct {
203  arrow: OperateIcon = null
204  icon: OperateIcon = null
205  subIcon: OperateIcon = null
206  button: OperateButton = null
207  switch: OperateCheck = null
208  checkBox: OperateCheck = null
209  radio: OperateCheck = null
210  image: Resource = null
211  text: string = null
212  @State switchState: boolean = false
213  @State radioState: boolean = false
214  @State checkBoxState: boolean = false
215  private rowSpace: number = DEFAULT_ROW_SPACE
216
217  aboutToAppear() {
218    if (this.switch != null) {
219      this.switchState = this.switch.isCheck
220    }
221    if (this.radio != null) {
222      this.radioState = this.radio.isCheck
223    }
224    if (this.checkBox != null) {
225      this.checkBoxState = this.checkBox.isCheck
226    }
227
228    if ((this.button == null && this.image == null && this.icon != null && this.text != null) ||
229      (this.button == null && this.image == null && this.icon == null && this.arrow != null && this.text != null)) {
230      this.rowSpace = SPECICAL_ROW_SPACE
231    }
232  }
233
234  @Builder
235  createButton(text: string) {
236    Button(text)
237      .fontSize($r('sys.float.ohos_id_text_size_button3'))
238      .fontColor($r('sys.color.ohos_id_color_text_primary_activated_transparent'))
239      .height(BUTTON_SIZE)
240      .backgroundColor($r('sys.color.ohos_id_color_button_normal'))
241      .labelStyle({
242        maxLines: TEXT_MAX_LINE
243      })
244  }
245
246  @Builder
247  createIcon(icon: OperateIcon) {
248    Row() {
249      Image(icon.value)
250        .height(OPERATEITEM_ICONLIKE_SIZE)
251        .width(OPERATEITEM_ICONLIKE_SIZE)
252        .focusable(true)
253        .fillColor($r('sys.color.ohos_id_color_primary'))
254    }
255    .height(OPERATEITEM_ICON_CLICKABLE_SIZE)
256    .width(OPERATEITEM_ICON_CLICKABLE_SIZE)
257    .justifyContent(FlexAlign.Center)
258    .onClick(icon.action)
259  }
260
261  @Builder
262  createImage(image: Resource) {
263    Image(image)
264      .height(OPERATEITEM_IMAGE_SIZE)
265      .width(OPERATEITEM_IMAGE_SIZE)
266  }
267
268  @Builder
269  createText(text: string) {
270    Text(text)
271      .fontSize($r('sys.float.ohos_id_text_size_body2'))
272      .fontColor($r('sys.color.ohos_id_color_text_secondary'))
273      .focusable(true)
274  }
275
276  @Builder
277  createArrow(icon: OperateIcon) {
278    Image(icon.value)
279      .height(OPERATEITEM_ICONLIKE_SIZE)
280      .width(OPERATEITEM_ARROW_WIDTH)
281      .focusable(true)
282      .fillColor($r('sys.color.ohos_id_color_fourth'))
283      .onClick(icon.action)
284  }
285
286  @Builder
287  createRadio(radio: OperateCheck) {
288    Radio({ value: null, group: null })
289      .checked(this.radioState)
290      .onChange(radio.onChange)
291      .height(OPERATEITEM_ICONLIKE_SIZE)
292      .width(OPERATEITEM_ICONLIKE_SIZE)
293  }
294
295  @Builder
296  createCheckBox(checkBox: OperateCheck) {
297    Checkbox()
298      .select(this.checkBoxState)
299      .onChange(checkBox.onChange)
300      .height(OPERATEITEM_ICONLIKE_SIZE)
301      .height(OPERATEITEM_ICONLIKE_SIZE)
302  }
303
304  @Builder
305  createSwitch(toggleParams: OperateCheck) {
306    Row() {
307      Toggle({ type: ToggleType.Switch, isOn: this.switchState })
308        .onChange(toggleParams.onChange)
309        .onClick(() => {
310          this.switchState = !this.switchState
311        })
312    }
313    .height(OPERATEITEM_ICON_CLICKABLE_SIZE)
314    .width(OPERATEITEM_ICON_CLICKABLE_SIZE)
315    .justifyContent(FlexAlign.Center)
316  }
317
318  build() {
319    Row({
320      space: this.rowSpace
321    }) {
322      if (this.button != null) {
323        this.createButton(this.button.text)
324      }
325
326      else if (this.image != null) {
327        this.createImage(this.image)
328      }
329      else if (this.icon != null && this.text != null) {
330        this.createText(this.text)
331        this.createIcon(this.icon)
332      }
333      else if (this.arrow != null && this.text == null) {
334        this.createArrow(this.arrow)
335      }
336      else if (this.arrow != null && this.text != null) {
337        this.createText(this.text)
338        this.createArrow(this.arrow)
339      }
340      else if (this.text != null) {
341        this.createText(this.text)
342      }
343      else if (this.radio != null) {
344        this.createRadio(this.radio)
345      }
346      else if (this.checkBox != null) {
347        this.createCheckBox(this.checkBox)
348      }
349      else if (this.switch != null) {
350        this.createSwitch(this.switch)
351      }
352      else if (this.icon != null) {
353        this.createIcon(this.icon)
354        if (this.subIcon != null) {
355          this.createIcon(this.subIcon)
356        }
357      }
358    }
359  }
360}
361
362/**
363 * Declare type OperateIcon
364 * @typedef OperationOption
365 * @syscap SystemCapability.ArkUI.ArkUI.Full
366 * @since 10
367 */
368export declare type OperateIcon = {
369  /**
370   * The content of text or the address of icon.
371   * @type { ResourceStr }.
372   * @since 10
373   */
374  value: ResourceStr,
375
376  /**
377   * Callback function when operate the icon.
378   * @type { () => void }.
379   * @since 10
380   */
381  action?: () => void
382}
383
384export type OperateButton = {
385  /**
386   * The text on the button.
387   * @type { ResourceStr }.
388   * @since 10
389   */
390  text?: string
391}
392
393/**
394 * Declare type OperateCheck
395 * @typedef OperationOption
396 * @syscap SystemCapability.ArkUI.ArkUI.Full
397 * @since 10
398 */
399export declare type OperateCheck = {
400  /**
401   * Whether is checked on default.
402   * @type { ResourceStr }.
403   * @since 10
404   */
405  isCheck?: boolean,
406
407  /**
408   * Callback function when operate the checkbox/switch/radio.
409   * @type { () => void }.
410   * @since 10
411   */
412  onChange?: (value: boolean) => void
413}
414
415@Component
416export struct ComposeListItem {
417  @Prop contentItem: ContentItem = null;
418  @Prop operateItem: OperateItem = null;
419  @State frontColor: string = Color.Transparent.toString()
420  @State borderSize: number = 0;
421  private isActive: boolean = false
422
423  build() {
424    Column() {
425      Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
426        if (this.contentItem == null) {
427          ContentItemStruct({
428            title: null
429          })
430        }
431        if (this.contentItem != null) {
432          ContentItemStruct({
433            icon: typeof this.contentItem.icon === 'string' ? null : this.contentItem.icon,
434            iconStyle: this.contentItem.iconStyle,
435            title: typeof this.contentItem.primaryText === 'string' ? this.contentItem.primaryText : null,
436            subtitle: typeof this.contentItem.secondaryText === 'string' ? this.contentItem.secondaryText : null,
437            description: typeof this.contentItem.description === 'string' ? this.contentItem.description : null
438          })
439        }
440        if (this.operateItem != null) {
441          OperateItemStruct({
442            icon: this.operateItem.icon,
443            subIcon: this.operateItem.subIcon,
444            button: this.operateItem.button,
445            switch: this.operateItem.switch,
446            checkBox: this.operateItem.checkbox,
447            radio: this.operateItem.radio,
448            image: typeof this.operateItem.image === 'string' ? null : this.operateItem.image,
449            text: typeof this.operateItem.text === 'string' ? this.operateItem.text : null,
450            arrow: typeof this.operateItem.arrow === 'string' ? null : this.operateItem.arrow
451          })
452        }
453      }
454      .focusable(true)
455      .border({
456        width: this.borderSize,
457        color: $r('sys.color.ohos_id_color_focused_outline')
458      })
459      .borderRadius($r('sys.float.ohos_id_corner_radius_default_m'))
460      .backgroundColor(this.frontColor)
461      .onFocus(() => {
462        this.borderSize = LISTITEMCARD_BORDER_SHOWN
463      })
464      .onBlur(() => {
465        this.borderSize = LISTITEMCARD_BORDER_HIDDEN
466      })
467      .onHover((isHover: boolean) => {
468        this.frontColor = isHover ? HOVERING_COLOR : (this.isActive ? ACTIVED_COLOR : Color.Transparent.toString())
469      })
470      .onTouch((event: TouchEvent) => {
471        if (event.type == TouchType.Down) {
472          this.frontColor = TOUCH_DOWN_COLOR
473        }
474        if (event.type == TouchType.Up) {
475          this.frontColor = this.isActive ? ACTIVED_COLOR : Color.Transparent.toString()
476        }
477      })
478      .onClick(() => {
479        this.isActive = !this.isActive
480        this.frontColor = this.isActive ? ACTIVED_COLOR : Color.Transparent.toString()
481      })
482    }
483    .padding({
484      left: $r('sys.float.ohos_id_default_padding_start'),
485      right: $r('sys.float.ohos_id_default_padding_end')
486    })
487  }
488}