1# 创建列表 (List) 2 3 4## 概述 5 6列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。 7 8使用列表可以轻松高效地显示结构化、可滚动的信息。通过在[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)组件中按垂直或者水平方向线性排列子组件[ListItemGroup](../reference/apis-arkui/arkui-ts/ts-container-listitemgroup.md)或[ListItem](../reference/apis-arkui/arkui-ts/ts-container-listitem.md),为列表中的行或列提供单个视图,或使用[循环渲染](../quick-start/arkts-rendering-control-foreach.md)迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等[渲染控制](../quick-start/arkts-rendering-control-overview.md)方式生成子组件。 9 10 11## 布局与约束 12 13列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。 14 15如下图所示,在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。 16 17ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。 18 19 **图1** List、ListItemGroup和ListItem组件关系 20 21 22 23>**说明:** 24> 25>List的子组件必须是ListItemGroup或ListItem,ListItem和ListItemGroup必须配合List来使用。 26 27 28### 布局 29 30List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应[延伸能力](../key-features/multi-device-app-dev/adaptive-layout.md#延伸能力)之外,还提供了自适应交叉轴方向上排列个数的布局能力。 31 32利用垂直布局能力可以构建单列或者多列垂直滚动列表,如下图所示。 33 34 **图2** 垂直滚动列表(左:单列;右:多列) 35 36 37 38利用水平布局能力可以是构建单行或多行水平滚动列表,如下图所示。 39 40 **图3** 水平滚动列表(左:单行;右:多行) 41 42 43 44 45Grid和WaterFlow也可以实现单列、多列布局,如果布局每列等宽,且不需要跨行跨列布局,相比Grid和WaterFlow,则更推荐使用List。 46 47### 约束 48 49列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。 50 51如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是垂直方向。 52 53 **图4** 列表的主轴与交叉轴 54 55 56 57如果List组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。 58 59如果List组件主轴方向没有设置尺寸,当List子组件主轴方向总尺寸小于List的父组件尺寸时,List主轴方向尺寸自动适应子组件的总尺寸。 60 61如下图所示,一个垂直列表B没有设置高度时,其父组件A高度为200vp,若其所有子组件C的高度总和为150vp,则此时列表B的高度为150vp。 62 63 **图5** 列表主轴高度约束示例1(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件) 64 65 66 67如果子组件主轴方向总尺寸超过List父组件尺寸时,List主轴方向尺寸适应List的父组件尺寸。 68 69如下图所示,同样是没有设置高度的垂直列表B,其父组件A高度为200vp,若其所有子组件C的高度总和为300vp,则此时列表B的高度为200vp。 70 71 **图6** 列表主轴高度约束示例2(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件) 72 73 74 75List组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。 76 77 78## 开发布局 79 80 81### 设置主轴方向 82 83List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表。 84 85若是水平滚动列表场景,将List的listDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。 86 87 88```ts 89List() { 90 // ... 91} 92.listDirection(Axis.Horizontal) 93``` 94 95 96### 设置交叉轴布局 97 98List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。 99 100List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如[歌单列表](../key-features/multi-device-app-dev/music-album-page.md#歌单列表)。lanes属性的取值类型是"number | [LengthConstrain](../reference/apis-arkui/arkui-ts/ts-types.md#lengthconstrain)",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表,如图2中右图所示。lanes的默认值为1,即默认情况下,垂直列表的列数是1。 101 102 103```ts 104List() { 105 // ... 106} 107.lanes(2) 108``` 109 110当其取值为LengthConstrain类型时,表示会根据LengthConstrain与List组件的尺寸自适应决定行或列数。 111 112 113```ts 114@Entry 115@Component 116struct EgLanes { 117 @State egLanes: LengthConstrain = { minLength: 200, maxLength: 300 } 118 build() { 119 List() { 120 // ... 121 } 122 .lanes(this.egLanes) 123 } 124} 125``` 126 127例如,假设在垂直列表中设置了lanes的值为{ minLength: 200, maxLength: 300 }。此时: 128 129- 当List组件宽度为300vp时,由于minLength为200vp,此时列表为一列。 130 131- 当List组件宽度变化至400vp时,符合两倍的minLength,则此时列表自适应为两列。 132 133同样以垂直列表为例,当alignListItem属性设置为ListItemAlign.Center表示列表项在水平方向上居中对齐。alignListItem的默认值是ListItemAlign.Start,即列表项在列表交叉轴方向上默认按首部对齐。 134 135 136```ts 137List() { 138 // ... 139} 140.alignListItem(ListItemAlign.Center) 141``` 142 143 144## 在列表中显示数据 145 146列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List静态地创建其列表项ListItem的内容。 147 148 **图7** 城市列表 149 150 151 152```ts 153@Entry 154@Component 155struct CityList { 156 build() { 157 List() { 158 ListItem() { 159 Text('北京').fontSize(24) 160 } 161 162 ListItem() { 163 Text('杭州').fontSize(24) 164 } 165 166 ListItem() { 167 Text('上海').fontSize(24) 168 } 169 } 170 .backgroundColor('#FFF1F3F5') 171 .alignListItem(ListItemAlign.Center) 172 } 173} 174``` 175 176由于在ListItem中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。 177 178 **图8** 联系人列表项示例 179 180 181 182如上图所示,联系人列表的列表项中,每个联系人都有头像和名称。此时,需要将Image和Text封装到一个Row容器内。 183 184 185```ts 186List() { 187 ListItem() { 188 Row() { 189 Image($r('app.media.iconE')) 190 .width(40) 191 .height(40) 192 .margin(10) 193 194 Text('小明') 195 .fontSize(20) 196 } 197 } 198 199 ListItem() { 200 Row() { 201 Image($r('app.media.iconF')) 202 .width(40) 203 .height(40) 204 .margin(10) 205 206 Text('小红') 207 .fontSize(20) 208 } 209 } 210} 211``` 212 213 214## 迭代列表内容 215 216通常,应用通过数据集合动态地创建列表。使用[循环渲染](../quick-start/arkts-rendering-control-foreach.md)可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。 217 218ArkTS通过[ForEach](../quick-start/arkts-rendering-control-foreach.md)提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以Contact类结构存储到contacts数组,使用ForEach中嵌套ListItem的形式来代替多个平铺的、内容相似的ListItem,从而减少重复代码。 219 220 221```ts 222import { util } from '@kit.ArkTS' 223 224class Contact { 225 key: string = util.generateRandomUUID(true); 226 name: string; 227 icon: Resource; 228 229 constructor(name: string, icon: Resource) { 230 this.name = name; 231 this.icon = icon; 232 } 233} 234 235@Entry 236@Component 237struct SimpleContacts { 238 private contacts: Array<object> = [ 239 new Contact('小明', $r("app.media.iconA")), 240 new Contact('小红', $r("app.media.iconB")), 241 ] 242 243 build() { 244 List() { 245 ForEach(this.contacts, (item: Contact) => { 246 ListItem() { 247 Row() { 248 Image(item.icon) 249 .width(40) 250 .height(40) 251 .margin(10) 252 Text(item.name).fontSize(20) 253 } 254 .width('100%') 255 .justifyContent(FlexAlign.Start) 256 } 257 }, (item: Contact) => JSON.stringify(item)) 258 } 259 .width('100%') 260 } 261} 262``` 263 264在List组件中,ForEach除了可以用来循环渲染ListItem,也可以用来循环渲染ListItemGroup。ListItemGroup的循环渲染详细使用请参见[支持分组列表](#支持分组列表)。 265 266 267## 自定义列表样式 268 269 270### 设置内容间距 271 272在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加10vp的间距: 273 274 275```ts 276List({ space: 10 }) { 277 // ... 278} 279``` 280 281 282### 添加分隔线 283 284分隔线用来将界面元素隔开,使单个元素更加容易识别。如下图所示,当列表项左边有图标(如蓝牙图标),由于图标本身就能很好的区分,此时分隔线从图标之后开始显示即可。 285 286 **图9** 设置列表分隔线样式 287 288 289 290List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidth和color属性设置分隔线的粗细和颜色。 291 292startMargin和endMargin属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。 293 294 295```ts 296class DividerTmp { 297 strokeWidth: Length = 1 298 startMargin: Length = 60 299 endMargin: Length = 10 300 color: ResourceColor = '#ffe9f0f0' 301 302 constructor(strokeWidth: Length, startMargin: Length, endMargin: Length, color: ResourceColor) { 303 this.strokeWidth = strokeWidth 304 this.startMargin = startMargin 305 this.endMargin = endMargin 306 this.color = color 307 } 308} 309@Entry 310@Component 311struct EgDivider { 312 @State egDivider: DividerTmp = new DividerTmp(1, 60, 10, '#ffe9f0f0') 313 build() { 314 List() { 315 // ... 316 } 317 .divider(this.egDivider) 318 } 319} 320``` 321 322此示例表示从距离列表侧边起始端60vp开始到距离结束端10vp的位置,画一条粗细为1vp的分割线,可以实现图9设置列表分隔线的样式。 323 324>**说明:** 325> 326>1. 分隔线的宽度会使ListItem之间存在一定间隔,当List设置的内容间距小于分隔线宽度时,ListItem之间的间隔会使用分隔线的宽度。 327> 328>2. 当List存在多列时,分割线的startMargin和endMargin作用于每一列上。 329> 330>3. List组件的分隔线画在两个ListItem之间,第一个ListItem上方和最后一个ListItem下方不会绘制分隔线。 331 332 333### 添加滚动条 334 335当列表项高度(宽度)超出屏幕高度(宽度)时,列表可以沿垂直(水平)方向滚动。在页面内容很多时,若用户需快速定位,可拖拽滚动条,如下图所示。 336 337 **图10** 列表的滚动条 338 339 340 341在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为[BarState](../reference/apis-arkui/arkui-ts/ts-appendix-enums.md#barstate),当取值为BarState.Auto表示按需显示滚动条。此时,当触摸到滚动条区域时显示控件,可上下拖拽滚动条快速浏览内容,拖拽时会变粗。若不进行任何操作,2秒后滚动条自动消失。 342 343scrollBar属性API version 9及以下版本默认值为BarState.Off,从API version 10版本开始默认值为BarState.Auto。 344```ts 345List() { 346 // ... 347} 348.scrollBar(BarState.Auto) 349``` 350 351 352## 支持分组列表 353 354在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见,如下图所示联系人列表。 355 356 **图11** 联系人分组列表 357 358 359 360在List组件中使用ListItemGroup对项目进行分组,可以构建二维列表。 361 362在List组件中可以直接使用一个或者多个ListItemGroup组件,ListItemGroup的宽度默认充满List组件。在初始化ListItemGroup时,可通过header参数设置列表分组的头部组件。 363 364 365```ts 366@Entry 367@Component 368struct ContactsList { 369 370 @Builder itemHead(text: string) { 371 // 列表分组的头部组件,对应联系人分组A、B等位置的组件 372 Text(text) 373 .fontSize(20) 374 .backgroundColor('#fff1f3f5') 375 .width('100%') 376 .padding(5) 377 } 378 379 build() { 380 List() { 381 ListItemGroup({ header: this.itemHead('A') }) { 382 // 循环渲染分组A的ListItem 383 } 384 385 ListItemGroup({ header: this.itemHead('B') }) { 386 // 循环渲染分组B的ListItem 387 } 388 } 389 } 390} 391``` 392 393如果多个ListItemGroup结构类似,可以将多个分组的数据组成数组,然后使用ForEach对多个分组进行循环渲染。例如在联系人列表中,将每个分组的联系人数据contacts(可参考[迭代列表内容](#迭代列表内容)章节)和对应分组的标题title数据进行组合,定义为数组contactsGroups。然后在ForEach中对contactsGroups进行循环渲染,即可实现多个分组的联系人列表。可参考[添加粘性标题](#添加粘性标题)章节示例代码。 394 395## 添加粘性标题 396 397粘性标题是一种常见的标题模式,常用于定位字母列表的头部元素。如下图所示,在联系人列表中滚动A部分时,B部分开始的头部元素始终处于A的下方。而在开始滚动B部分时,B的头部会固定在屏幕顶部,直到所有B的项均完成滚动后,才被后面的头部替代。 398 399粘性标题不仅有助于阐明列表中数据的表示形式和用途,还可以帮助用户在大量信息中进行数据定位,从而避免用户在标题所在的表的顶部与感兴趣区域之间反复滚动。 400 401 **图12** 粘性标题 402 403 404 405List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。 406 407通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer。 408 409 410```ts 411import { util } from '@kit.ArkTS' 412class Contact { 413 key: string = util.generateRandomUUID(true); 414 name: string; 415 icon: Resource; 416 417 constructor(name: string, icon: Resource) { 418 this.name = name; 419 this.icon = icon; 420 } 421} 422class ContactsGroup { 423 title: string = '' 424 contacts: Array<object> | null = null 425 key: string = "" 426} 427export let contactsGroups: object[] = [ 428 { 429 title: 'A', 430 contacts: [ 431 new Contact('艾佳', $r('app.media.iconA')), 432 new Contact('安安', $r('app.media.iconB')), 433 new Contact('Angela', $r('app.media.iconC')), 434 ], 435 key: util.generateRandomUUID(true) 436 } as ContactsGroup, 437 { 438 title: 'B', 439 contacts: [ 440 new Contact('白叶', $r('app.media.iconD')), 441 new Contact('伯明', $r('app.media.iconE')), 442 ], 443 key: util.generateRandomUUID(true) 444 } as ContactsGroup, 445 // ... 446] 447@Entry 448@Component 449struct ContactsList { 450 // 定义分组联系人数据集合contactsGroups数组 451 @Builder itemHead(text: string) { 452 // 列表分组的头部组件,对应联系人分组A、B等位置的组件 453 Text(text) 454 .fontSize(20) 455 .backgroundColor('#fff1f3f5') 456 .width('100%') 457 .padding(5) 458 } 459 build() { 460 List() { 461 // 循环渲染ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合 462 ForEach(contactsGroups, (itemGroup: ContactsGroup) => { 463 ListItemGroup({ header: this.itemHead(itemGroup.title) }) { 464 // 循环渲染ListItem 465 if (itemGroup.contacts) { 466 ForEach(itemGroup.contacts, (item: Contact) => { 467 ListItem() { 468 // ... 469 } 470 }, (item: Contact) => JSON.stringify(item)) 471 } 472 } 473 }, (itemGroup: ContactsGroup) => JSON.stringify(itemGroup)) 474 }.sticky(StickyStyle.Header) // 设置吸顶,实现粘性标题效果 475 } 476} 477``` 478 479 480## 控制滚动位置 481 482控制滚动位置在实际应用中十分常见,例如当新闻页列表项数量庞大,用户滚动列表到一定位置时,希望快速滚动到列表底部或返回列表顶部。此时,可以通过控制滚动位置来实现列表的快速定位,如下图所示。 483 484 **图13** 返回列表顶部 485 486 487 488List组件初始化时,可以通过scroller参数绑定一个[Scroller](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scroller)对象,进行列表的滚动控制。例如,用户在新闻应用中,点击新闻页面底部的返回顶部按钮时,就可以通过Scroller对象的scrollToIndex方法使列表滚动到指定的列表项索引位置。 489 490首先,需要创建一个Scroller的对象listScroller。 491 492 493```ts 494private listScroller: Scroller = new Scroller(); 495``` 496 497然后,通过将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。在需要跳转的位置指定scrollToIndex的参数为0,表示返回列表顶部。 498 499 500```ts 501Stack({ alignContent: Alignment.Bottom }) { 502 // 将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。 503 List({ space: 20, scroller: this.listScroller }) { 504 // ... 505 } 506 507 Button() { 508 // ... 509 } 510 .onClick(() => { 511 // 点击按钮时,指定跳转位置,返回列表顶部 512 this.listScroller.scrollToIndex(0) 513 }) 514} 515``` 516 517 518## 响应滚动位置 519 520许多应用需要监听列表的滚动位置变化并作出响应。例如,在联系人列表滚动时,如果跨越了不同字母开头的分组,则侧边字母索引栏也需要更新到对应的字母位置。 521 522除了字母索引之外,滚动列表结合多级分类索引在应用开发过程中也很常见,例如购物应用的商品分类页面,多级分类也需要监听列表的滚动位置。 523 524**图14** 字母索引响应联系人列表滚动 525 526 527 528如上图所示,当联系人列表从A滚动到B时,右侧索引栏也需要同步从选中A状态变成选中B状态。此场景可以通过监听List组件的onScrollIndex事件来实现,右侧索引栏需要使用字母表索引组件[AlphabetIndexer](../reference/apis-arkui/arkui-ts/ts-container-alphabet-indexer.md)。 529 530在列表滚动时,根据列表此时所在的索引值位置firstIndex,重新计算字母索引栏对应字母的位置selectedIndex。由于AlphabetIndexer组件通过selected属性设置了选中项索引值,当selectedIndex变化时会触发AlphabetIndexer组件重新渲染,从而显示为选中对应字母的状态。 531 532 533```ts 534const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 535 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 536@Entry 537@Component 538struct ContactsList { 539 @State selectedIndex: number = 0; 540 private listScroller: Scroller = new Scroller(); 541 542 build() { 543 Stack({ alignContent: Alignment.End }) { 544 List({ scroller: this.listScroller }) {} 545 .onScrollIndex((firstIndex: number) => { 546 // 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置this.selectedIndex 547 }) 548 549 // 字母表索引组件 550 AlphabetIndexer({ arrayValue: alphabets, selected: 0 }) 551 .selected(this.selectedIndex) 552 } 553 } 554} 555``` 556 557>**说明:** 558> 559>计算索引值时,ListItemGroup作为一个整体占一个索引值,不计算ListItemGroup内部ListItem的索引值。 560 561 562## 响应列表项侧滑 563 564侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息,如下图所示。其中,列表项头像右上角标记设置参考[给列表项添加标记](#给列表项添加标记)。 565 566**图15** 侧滑删除列表项 567 568 569 570ListItem的[swipeAction属性](../reference/apis-arkui/arkui-ts/ts-container-listitem.md#swipeaction9)可用于实现列表项的左右滑动功能。swipeAction属性方法初始化时有必填参数SwipeActionOptions,其中,start参数表示设置列表项右滑时起始端滑出的组件,end参数表示设置列表项左滑时尾端滑出的组件。 571 572在消息列表中,end参数表示设置ListItem左滑时尾端划出自定义组件,即删除按钮。在初始化end方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。 573 5741. 实现尾端滑出组件的构建。 575 576 ```ts 577 @Builder itemEnd(index: number) { 578 // 构建尾端滑出组件 579 Button({ type: ButtonType.Circle }) { 580 Image($r('app.media.ic_public_delete_filled')) 581 .width(20) 582 .height(20) 583 } 584 .onClick(() => { 585 // this.messages为列表数据源,可根据实际场景构造。点击后从数据源删除指定数据项。 586 this.messages.splice(index, 1); 587 }) 588 } 589 ``` 590 5912. 绑定swipeAction属性到可左滑的ListItem上。 592 593 ```ts 594 // 构建List时,通过ForEach基于数据源this.messages循环渲染ListItem。 595 ListItem() { 596 // ... 597 } 598 .swipeAction({ 599 end: { 600 // index为该ListItem在List中的索引值。 601 builder: () => { this.itemEnd(index) }, 602 } 603 }) // 设置侧滑属性. 604 ``` 605 606## 给列表项添加标记 607 608添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息,如下图所示。 609 610 **图16** 给列表项添加标记 611 612 613 614在ListItem中使用[Badge](../reference/apis-arkui/arkui-ts/ts-container-badge.md)组件可实现给列表项添加标记功能。Badge是可以附加在单个组件上用于信息标记的容器组件。 615 616在消息列表中,若希望在联系人头像右上角添加标记,可在实现消息列表项ListItem的联系人头像时,将头像Image组件作为Badge的子组件。 617 618在Badge组件中,count和position参数用于设置需要展示的消息数量和提示点显示位置,还可以通过style参数灵活设置标记的样式。 619 620 621```ts 622ListItem() { 623 Badge({ 624 count: 1, 625 position: BadgePosition.RightTop, 626 style: { badgeSize: 16, badgeColor: '#FA2A2D' } 627 }) { 628 // Image组件实现消息联系人头像 629 // ... 630 } 631} 632``` 633 634 635## 下拉刷新与上拉加载 636 637页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md),在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。 638 639以下拉刷新为例,其实现主要分成三步: 640 6411. 监听手指按下事件,记录其初始位置的值。 642 6432. 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。 644 6453. 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。 646 647> **说明:** 648> 649> 页面的下拉刷新操作推荐使用[Refresh](../reference/apis-arkui/arkui-ts/ts-container-refresh.md)组件实现。 650 651<!--RP1--><!--RP1End--> 652 653<!--Del--> 654下拉刷新与上拉加载的具体实现可参考[相关实例](#相关实例)中新闻数据加载。若开发者希望快速实现此功能,也可使用三方组件[PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh)。<!--DelEnd--> 655 656 657## 编辑列表 658 659列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。 660 661下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。 662 663 664### 新增列表项 665 666如下图所示,当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。 667 668 **图17** 新增待办 669 670 671 672添加列表项功能实现主要流程如下: 673 6741. 定义列表项数据结构,以待办事项管理为例,首先定义待办数据结构。 675 676 ```ts 677 //ToDo.ets 678 import { util } from '@kit.ArkTS' 679 680 export class ToDo { 681 key: string = util.generateRandomUUID(true); 682 name: string; 683 684 constructor(name: string) { 685 this.name = name; 686 } 687 } 688 ``` 689 6902. 构建列表整体布局和列表项。 691 692 ```ts 693 //ToDoListItem.ets 694 import { ToDo } from './ToDo'; 695 @Component 696 export struct ToDoListItem { 697 @Link isEditMode: boolean 698 @Link selectedItems: ToDo[] 699 private toDoItem: ToDo = new ToDo(""); 700 701 build() { 702 Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { 703 // ... 704 } 705 .width('100%') 706 .height(80) 707 //.padding() 根据具体使用场景设置 708 .borderRadius(24) 709 //.linearGradient() 根据具体使用场景设置 710 .gesture( 711 GestureGroup(GestureMode.Exclusive, 712 LongPressGesture() 713 .onAction(() => { 714 // ... 715 }) 716 ) 717 ) 718 } 719 } 720 ``` 721 7223. 初始化待办列表数据和可选事项,最后,构建列表布局和列表项。 723 724 ```ts 725 //ToDoList.ets 726 import { ToDo } from './ToDo'; 727 import { ToDoListItem } from './ToDoListItem'; 728 729 @Entry 730 @Component 731 struct ToDoList { 732 @State toDoData: ToDo[] = [] 733 @Watch('onEditModeChange') @State isEditMode: boolean = false 734 @State selectedItems: ToDo[] = [] 735 private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌'] 736 737 onEditModeChange() { 738 if (!this.isEditMode) { 739 this.selectedItems = [] 740 } 741 } 742 743 build() { 744 Column() { 745 Row() { 746 if (this.isEditMode) { 747 Text('X') 748 .fontSize(20) 749 .onClick(() => { 750 this.isEditMode = false; 751 }) 752 .margin({ left: 20, right: 20 }) 753 } else { 754 Text('待办') 755 .fontSize(36) 756 .margin({ left: 40 }) 757 Blank() 758 Text('+') //提供新增列表项入口,即给新增按钮添加点击事件 759 .onClick(() => { 760 this.getUIContext().showTextPickerDialog({ 761 range: this.availableThings, 762 onAccept: (value: TextPickerResult) => { 763 let arr = Array.isArray(value.index) ? value.index : [value.index]; 764 for (let i = 0; i < arr.length; i++) { 765 this.toDoData.push(new ToDo(this.availableThings[arr[i]])); // 新增列表项数据toDoData(可选事项) 766 } 767 }, 768 }) 769 }) 770 } 771 List({ space: 10 }) { 772 ForEach(this.toDoData, (toDoItem: ToDo) => { 773 ListItem() { 774 // 将toDoData的每个数据放入到以model的形式放进ListItem里 775 ToDoListItem({ 776 isEditMode: this.isEditMode, 777 toDoItem: toDoItem, 778 selectedItems: this.selectedItems }) 779 } 780 }, (toDoItem: ToDo) => toDoItem.key.toString()) 781 } 782 } 783 } 784 } 785 } 786 ``` 787 788 789### 删除列表项 790 791如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。 792 793 **图18** 长按删除待办事项 794 795 796 797删除列表项功能实现主要流程如下: 798 7991. 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。 800 以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。 801 802 ```ts 803 // 结构参考 804 export class ToDo { 805 key: string = util.generateRandomUUID(true); 806 name: string; 807 toDoData: ToDo[] = []; 808 809 constructor(name: string) { 810 this.name = name; 811 } 812 } 813 ``` 814 ```ts 815 // 实现参考 816 Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { 817 // ... 818 } 819 .gesture( 820 GestureGroup(GestureMode.Exclusive, 821 LongPressGesture() 822 .onAction(() => { 823 if (!this.isEditMode) { 824 this.isEditMode = true; //进入编辑模式 825 } 826 }) 827 ) 828 ) 829 ``` 830 8312. 需要响应用户的选择交互,记录要删除的列表项数据。 832 在待办列表中,通过勾选框的勾选或取消勾选,响应用户勾选列表项变化,记录所有选择的列表项。 833 834 ```ts 835 // 结构参考 836 import { util } from '@kit.ArkTS' 837 export class ToDo { 838 key: string = util.generateRandomUUID(true); 839 name: string; 840 toDoData: ToDo[] = []; 841 842 constructor(name: string) { 843 this.name = name; 844 } 845 } 846 ``` 847 ```ts 848 // 实现参考 849 if (this.isEditMode) { 850 Checkbox() 851 .onChange((isSelected) => { 852 if (isSelected) { 853 this.selectedItems.push(toDoList.toDoItem) // this.selectedItems为勾选时,记录选中的列表项,可根据实际场景构造 854 } else { 855 let index = this.selectedItems.indexOf(toDoList.toDoItem) 856 if (index !== -1) { 857 this.selectedItems.splice(index, 1) // 取消勾选时,则将此项从selectedItems中删除 858 } 859 } 860 }) 861 } 862 ``` 863 8643. 需要响应用户点击删除按钮事件,删除列表中对应的选项。 865 866 ```ts 867 // 结构参考 868 import { util } from '@kit.ArkTS' 869 export class ToDo { 870 key: string = util.generateRandomUUID(true); 871 name: string; 872 toDoData: ToDo[] = []; 873 874 constructor(name: string) { 875 this.name = name; 876 } 877 } 878 ``` 879 ```ts 880 // 实现参考 881 Button('删除') 882 .onClick(() => { 883 // this.toDoData为待办的列表项,可根据实际场景构造。点击后删除选中的列表项对应的toDoData数据 884 let leftData = this.toDoData.filter((item) => { 885 return !this.selectedItems.find((selectedItem) => selectedItem == item); 886 }) 887 this.toDoData = leftData; 888 this.isEditMode = false; 889 }) 890 ``` 891 892 893## 长列表的处理 894 895[循环渲染](../quick-start/arkts-rendering-control-foreach.md)适用于短列表,当构建具有大量列表项的长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,会导致页面启动时间过长,影响用户体验。因此,推荐使用[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)(LazyForEach)方式实现按需迭代加载数据,从而提升列表性能。 896 897关于长列表按需加载优化的具体实现可参考[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)章节中的示例。 898 899当使用懒加载方式渲染列表时,为了更好的列表滚动体验,减少列表滑动时出现白块,List组件提供了cachedCount参数用于设置列表项缓存数,只在懒加载LazyForEach中生效。 900 901 902```ts 903List() { 904 // ... 905}.cachedCount(3) 906``` 907 908以垂直列表为例: 909 910- 若懒加载是用于ListItem,当列表为单列模式时,会在List显示的ListItem前后各缓存cachedCount个ListItem;若是多列模式下,会在List显示的ListItem前后各缓存cachedCount \* 列数个ListItem。 911 912- 若懒加载是用于ListItemGroup,无论单列模式还是多列模式,都是在List显示的ListItem前后各缓存cachedCount个ListItemGroup。 913 914>**说明:** 915> 916>1. cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。 917> 918>2. 列表使用数据懒加载时,除了显示区域的列表项和前后缓存的列表项,其他列表项会被销毁。 919 920 921## 折叠与展开 922 923列表项的折叠与展开用途广泛,常用于信息清单的展示、填写等应用场景。 924 925 **图19** 列表项的折叠与展开 926 927 928 929列表项折叠与展开效果实现主要流程如下: 930 9311. 定义列表项数据结构。 932 933 ```ts 934 interface ItemInfo { 935 index: number, 936 name: string, 937 label: ResourceStr, 938 type?: string, 939 } 940 941 interface ItemGroupInfo extends ItemInfo { 942 children: ItemInfo[] 943 } 944 ``` 945 9462. 构造列表结构。 947 948 ```ts 949 @State routes: ItemGroupInfo[] = [ 950 { 951 index: 0, 952 name: 'basicInfo', 953 label: '个人基本资料', 954 children: [ 955 { 956 index: 0, 957 name: '昵称', 958 label: 'xxxx', 959 type: 'Text' 960 }, 961 { 962 index: 1, 963 name: '头像', 964 label: $r('sys.media.ohos_user_auth_icon_face'), 965 type: 'Image' 966 }, 967 { 968 index: 2, 969 name: '年龄', 970 label: 'xxxx', 971 type: 'Text' 972 }, 973 { 974 index: 3, 975 name: '生日', 976 label: 'xxxxxxxxx', 977 type: 'Text' 978 }, 979 { 980 index: 4, 981 name: '性别', 982 label: 'xxxxxxxx', 983 type: 'Text' 984 }, 985 ] 986 }, 987 { 988 index: 1, 989 name: 'equipInfo', 990 label: '设备信息', 991 children: [] 992 }, 993 { 994 index: 2, 995 name: 'appInfo', 996 label: '应用使用信息', 997 children: [] 998 }, 999 { 1000 index: 3, 1001 name: 'uploadInfo', 1002 label: '您主动上传的数据', 1003 children: [] 1004 }, 1005 { 1006 index: 4, 1007 name: 'tradeInfo', 1008 label: '交易与资产信息', 1009 children: [] 1010 }, 1011 { 1012 index: 5, 1013 name: 'otherInfo', 1014 label: '其他资料', 1015 children: [] 1016 }, 1017 ]; 1018 @State expandedItems: boolean[] = Array(this.routes.length).fill(false); 1019 @State selection: string | null = null; 1020 build() { 1021 Column() { 1022 // ... 1023 1024 List({ space: 10 }) { 1025 ForEach(this.routes, (itemGroup: ItemGroupInfo) => { 1026 ListItemGroup({ 1027 header: this.ListItemGroupHeader(itemGroup), 1028 style: ListItemGroupStyle.CARD, 1029 }) { 1030 if (this.expandedItems[itemGroup.index] && itemGroup.children) { 1031 ForEach(itemGroup.children, (item: ItemInfo) => { 1032 ListItem({ style: ListItemStyle.CARD }) { 1033 Row() { 1034 Text(item.name) 1035 Blank() 1036 if (item.type === 'Image') { 1037 Image(item.label) 1038 .height(20) 1039 .width(20) 1040 } else { 1041 Text(item.label) 1042 } 1043 Image($r('sys.media.ohos_ic_public_arrow_right')) 1044 .fillColor($r('sys.color.ohos_id_color_fourth')) 1045 .height(30) 1046 .width(30) 1047 } 1048 .width("100%") 1049 } 1050 .width("100%") 1051 .animation({ curve: curves.interpolatingSpring(0, 1, 528, 39) }) 1052 }) 1053 } 1054 }.clip(true) 1055 }) 1056 } 1057 .width("100%") 1058 } 1059 .width('100%') 1060 .height('100%') 1061 .justifyContent(FlexAlign.Start) 1062 .backgroundColor($r('sys.color.ohos_id_color_sub_background')) 1063 } 1064 ``` 1065 10663. 通过改变ListItem的状态,来控制每个列表项是否展开,并通过animation和animateTo来实现展开与折叠过程中的动效效果。 1067 1068 ```ts 1069 @Builder 1070 ListItemGroupHeader(itemGroup: ItemGroupInfo) { 1071 Row() { 1072 Text(itemGroup.label) 1073 Blank() 1074 Image($r('sys.media.ohos_ic_public_arrow_down')) 1075 .fillColor($r('sys.color.ohos_id_color_fourth')) 1076 .height(30) 1077 .width(30) 1078 .rotate({ angle: !!itemGroup.children.length ? (this.expandedItems[itemGroup.index] ? 180 : 0) : 180 }) 1079 .animation({ curve: curves.interpolatingSpring(0, 1, 228, 22) }) 1080 } 1081 .width("100%") 1082 .padding(10) 1083 .animation({ curve: curves.interpolatingSpring(0, 1, 528, 39) }) 1084 .onClick(() => { 1085 if (itemGroup.children.length) { 1086 this.getUIContext()?.animateTo({ curve: curves.interpolatingSpring(0, 1, 528, 39) }, () => { 1087 this.expandedItems[itemGroup.index] = !this.expandedItems[itemGroup.index] 1088 }) 1089 } 1090 }) 1091 } 1092 ``` 1093 1094## 相关实例 1095 1096如需详细了解ArkUI中列表的创建与使用,请参考以下示例: 1097 1098- [新闻数据加载](https://gitee.com/openharmony/codelabs/tree/master/NetworkManagement/NewsDataArkTS) 1099 1100- [音乐专辑页](../key-features/multi-device-app-dev/music-album-page.md) 1101 1102- [常用组件和容器低代码开发示例(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/EfficiencyEnhancementKit/SuperVisualSample) 1103 1104- [二级联动(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SecondLevelLinkage) 1105 1106- [List组件的使用之商品列表(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List) 1107 1108- [List组件的使用之设置项(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List_HDC) 1109 1110- [PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh) 1111 1112<!--RP2--><!--RP2End--> 1113