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}