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![zh-cn_image_0000001562940589](figures/zh-cn_image_0000001562940589.png)
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![zh-cn_image_0000001511580940](figures/zh-cn_image_0000001511580940.png)
37
38利用水平布局能力可以是构建单行或多行水平滚动列表,如下图所示。
39
40  **图3** 水平滚动列表(左:单行;右:多行)  
41
42![zh-cn_image_0000001511421344](figures/zh-cn_image_0000001511421344.png)
43
44
45Grid和WaterFlow也可以实现单列、多列布局,如果布局每列等宽,且不需要跨行跨列布局,相比Grid和WaterFlow,则更推荐使用List。
46
47### 约束
48
49列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。
50
51如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是垂直方向。
52
53  **图4** 列表的主轴与交叉轴  
54
55![zh-cn_image_0000001562940581](figures/zh-cn_image_0000001562940581.png)
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![zh-cn_image_0000001511580956](figures/zh-cn_image_0000001511580956.png)
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![zh-cn_image_0000001511740548](figures/zh-cn_image_0000001511740548.png)
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![zh-cn_image_0000001563060761](figures/zh-cn_image_0000001563060761.png)
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![zh-cn_image_0000001511421328](figures/zh-cn_image_0000001511421328.png)
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![zh-cn_image_0000001511580960](figures/zh-cn_image_0000001511580960.png)
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![zh-cn_image_0000001511740544](figures/zh-cn_image_0000001511740544.gif)
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.Auto344```ts
345List() {
346  // ...
347}
348.scrollBar(BarState.Auto)
349```
350
351
352## 支持分组列表
353
354在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见,如下图所示联系人列表。
355
356  **图11** 联系人分组列表 
357
358![zh-cn_image_0000001511580948](figures/zh-cn_image_0000001511580948.png)
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![zh-cn_image_0000001511740552](figures/zh-cn_image_0000001511740552.gif)
404
405List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。
406
407通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer408
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![zh-cn_image_0000001511900520](figures/zh-cn_image_0000001511900520.gif)
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![zh-cn_image_0000001563060769](figures/zh-cn_image_0000001563060769.gif)
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![zh-cn_image_0000001563060773](figures/zh-cn_image_0000001563060773.gif)
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![zh-cn_image_0000001511580952](figures/zh-cn_image_0000001511580952.png)
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![zh-cn_image_0000001511740556](figures/zh-cn_image_0000001511740556.gif)
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![zh-cn_image_0000001562820877](figures/zh-cn_image_0000001562820877.gif)
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![zh-cn_image_0000001949866104](figures/zh-cn_image_0000001949866104.gif)
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