• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 创建列表 (List)
2
3<!--Kit: ArkUI-->
4<!--Subsystem: ArkUI-->
5<!--Owner: @yylong-->
6<!--Designer: @yylong-->
7<!--Tester: @liuzhenshuo-->
8<!--Adviser: @HelloCrease-->
9
10## 概述
11
12列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。
13
14使用列表可以轻松高效地显示结构化、可滚动的信息。通过在[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),为列表中的行或列提供单个视图,或使用[循环渲染](../ui/state-management/arkts-rendering-control-foreach.md)迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等[渲染控制](../ui/state-management/arkts-rendering-control-overview.md)方式生成子组件。
15
16在圆形屏幕设备上,推荐使用[ArcList](../reference/apis-arkui/arkui-ts/ts-container-arclist.md)组件,使用方式可参考[创建弧形列表 (ArcList)](./arkts-layout-development-create-arclist.md)。
17
18## 布局与约束
19
20列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。
21
22如下图所示,在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。
23
24ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。
25
26  **图1** List、ListItemGroup和ListItem组件关系  
27
28![zh-cn_image_0000001562940589](figures/zh-cn_image_0000001562940589.png)
29
30>**说明:**
31>
32>List的子组件必须是ListItemGroup或ListItem,ListItem和ListItemGroup必须配合List来使用。
33
34
35### 布局
36
37List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应延伸能力之外,还提供了自适应交叉轴方向上排列个数的布局能力。
38
39利用垂直布局能力可以构建单列或者多列垂直滚动列表,如下图所示。
40
41  **图2** 垂直滚动列表(左:单列;右:多列)  
42
43![zh-cn_image_0000001511580940](figures/zh-cn_image_0000001511580940.png)
44
45利用水平布局能力可以是构建单行或多行水平滚动列表,如下图所示。
46
47  **图3** 水平滚动列表(左:单行;右:多行)  
48
49![zh-cn_image_0000001511421344](figures/zh-cn_image_0000001511421344.png)
50
51
52Grid和WaterFlow也可以实现单列、多列布局,如果布局每列等宽,且不需要跨行跨列布局,相比Grid和WaterFlow,则更推荐使用List。
53
54### 约束
55
56列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。
57
58如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是垂直方向。
59
60  **图4** 列表的主轴与交叉轴  
61
62![zh-cn_image_0000001562940581](figures/zh-cn_image_0000001562940581.png)
63
64如果List组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。
65
66如果List组件主轴方向没有设置尺寸,当List子组件主轴方向总尺寸小于List的父组件尺寸时,List主轴方向尺寸自动适应子组件的总尺寸。
67
68如下图所示,一个垂直列表B没有设置高度时,其父组件A高度为200vp,若其所有子组件C的高度总和为150vp,则此时列表B的高度为150vp。
69
70  **图5** 列表主轴高度约束示例1(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件)  
71
72![zh-cn_image_0000001511580956](figures/zh-cn_image_0000001511580956.png)
73
74如果子组件主轴方向总尺寸超过List父组件尺寸时,List主轴方向尺寸适应List的父组件尺寸。
75
76如下图所示,同样是没有设置高度的垂直列表B,其父组件A高度为200vp,若其所有子组件C的高度总和为300vp,则此时列表B的高度为200vp。
77
78  **图6** 列表主轴高度约束示例2(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件)  
79
80![zh-cn_image_0000001511740548](figures/zh-cn_image_0000001511740548.png)
81
82List组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。
83
84
85## 开发布局
86
87
88### 设置主轴方向
89
90List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表。
91
92若是水平滚动列表场景,将List的listDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。
93
94
95```ts
96List() {
97  // ...
98}
99.listDirection(Axis.Horizontal)
100```
101
102
103### 设置交叉轴布局
104
105List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。
106
107List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景。lanes属性的取值类型是"number | [LengthConstrain](../reference/apis-arkui/arkui-ts/ts-types.md#lengthconstrain)",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表,如图2中右图所示。lanes的默认值为1,即默认情况下,垂直列表的列数是1。
108
109
110```ts
111List() {
112  // ...
113}
114.lanes(2)
115```
116
117当其取值为LengthConstrain类型时,表示会根据LengthConstrain与List组件的尺寸自适应决定行或列数。
118
119
120```ts
121@Entry
122@Component
123struct EgLanes {
124  @State egLanes: LengthConstrain = { minLength: 200, maxLength: 300 };
125  build() {
126    List() {
127      // ...
128    }
129    .lanes(this.egLanes)
130  }
131}
132```
133
134例如,假设在垂直列表中设置了lanes的值为{ minLength: 200, maxLength: 300 }。此时:
135
136- 当List组件宽度为300vp时,由于minLength为200vp,此时列表为一列。
137
138- 当List组件宽度变化至400vp时,符合两倍的minLength,则此时列表自适应为两列。
139
140同样以垂直列表为例,当alignListItem属性设置为ListItemAlign.Center表示列表项在水平方向上居中对齐。alignListItem的默认值是ListItemAlign.Start,即列表项在列表交叉轴方向上默认按首部对齐。
141
142
143```ts
144List() {
145  // ...
146}
147.alignListItem(ListItemAlign.Center)
148```
149
150## ListItem生命周期
151### 使用ForEach创建ListItem
152List组件创建时,所有ListItem将会被创建。显示区域内的ListItem在首帧进行布局,预加载范围内的ListItem在空闲时完成布局。预加载范围之外的ListItem仅创建ListItem自身,ListItem其内部的子组件不会被创建。
153
154当List组件滑动时,进入预加载及显示区域的ListItem将会创建其内部的子组件并完成布局,而滑出预加载及显示区域的ListItem将不会被销毁。
155
156**图7** ForEach创建ListItem的生命周期
157![](./figures/list_foreach.png)
158
159### 使用LazyForEach创建ListItem
160List组件创建时,显示区域中的ListItem会被创建与布局。预加载范围内的ListItem在空闲时创建与布局,但是不会被挂载到组件树上。预加载范围外的ListItem则不会被创建。
161
162当List组件滑动时,进入预加载及显示区域的ListItem将被创建与布局,创建ListItem过程中,若ListItem内部如果包含@Reusable标记的自定义组件,则会优先从缓存池中复用。滑出预加载及显示区域的ListItem将被销毁,其内部若含@Reusable标记的自定义组件,则会被回收并加入缓存池。
163
164**图8** LazyForEach创建ListItem的生命周期
165![](./figures/list_lazyforeach.png)
166
167### 使用Repeat创建ListItem
168**使用virtualScroll**
169
170List组件创建时,显示区域内的ListItem将被创建和布局。预加载范围内的ListItem在空闲时创建和布局,并且挂载至组件树上。预加载范围外的ListItem则不会被创建。
171
172当List组件滑动时,进入预加载及显示区域的ListItem,将从缓存池中获取ListItem并复用及布局,若缓存池中无ListItem,则会新创建并布局。滑出预加载及显示区域的ListItem会将被回收至缓存池。
173
174**图9** Repeat使用virtualScroll创建ListItem的生命周期
175![](./figures/list_repeatv.png)
176
177**不使用virtualScroll**
178
179List组件创建时,所有ListItem均被创建。显示区域内的ListItem在首帧完成布局,预加载范围内的ListItem在空闲时完成布局。预加载范围外的ListItem不会进行布局。
180
181当List组件滑动时,进入预加载及显示区域的ListItem将进行布局。滑出预加载及显示区域的ListItem不会销毁。
182
183**图10** Repeat不使用virtualScroll创建ListItem的生命周期
184![](./figures/list_repeat.png)
185
186
187## 在列表中显示数据
188
189列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List静态地创建其列表项ListItem的内容。
190
191  **图11** 城市列表  
192
193![zh-cn_image_0000001563060761](figures/zh-cn_image_0000001563060761.png)
194
195```ts
196@Entry
197@Component
198struct CityList {
199  build() {
200    List() {
201      ListItem() {
202        Text('北京').fontSize(24)
203      }
204
205      ListItem() {
206        Text('杭州').fontSize(24)
207      }
208
209      ListItem() {
210        Text('上海').fontSize(24)
211      }
212    }
213    .backgroundColor('#FFF1F3F5')
214    .alignListItem(ListItemAlign.Center)
215  }
216}
217```
218
219由于在ListItem中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。
220
221  **图12** 联系人列表项示例  
222
223![zh-cn_image_0000001511421328](figures/zh-cn_image_0000001511421328.png)
224
225如上图所示,联系人列表的列表项中,每个联系人都有头像和名称。此时,需要将Image和Text封装到一个Row容器内。
226
227
228```ts
229List() {
230  ListItem() {
231    Row() {
232      // app.media.iconE为自定义资源
233      Image($r('app.media.iconE'))
234        .width(40)
235        .height(40)
236        .margin(10)
237
238      Text('小明')
239        .fontSize(20)
240    }
241  }
242
243  ListItem() {
244    Row() {
245      // app.media.iconF为自定义资源
246      Image($r('app.media.iconF'))
247        .width(40)
248        .height(40)
249        .margin(10)
250
251      Text('小红')
252        .fontSize(20)
253    }
254  }
255}
256```
257
258
259## 迭代列表内容
260
261通常,应用通过数据集合动态地创建列表。使用[循环渲染](../ui/state-management/arkts-rendering-control-foreach.md)可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。
262
263ArkTS通过[ForEach](../ui/state-management/arkts-rendering-control-foreach.md)提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以Contact类结构存储到contacts数组,使用ForEach中嵌套ListItem的形式来代替多个平铺的、内容相似的ListItem,从而减少重复代码。
264
265
266```ts
267import { util } from '@kit.ArkTS';
268
269class Contact {
270  key: string = util.generateRandomUUID(true);
271  name: string;
272  icon: Resource;
273
274  constructor(name: string, icon: Resource) {
275    this.name = name;
276    this.icon = icon;
277  }
278}
279
280@Entry
281@Component
282struct SimpleContacts {
283  private contacts: Array<object> = [
284    new Contact('小明', $r("app.media.iconA")),
285    new Contact('小红', $r("app.media.iconB")),
286  ];
287
288  build() {
289    List() {
290      ForEach(this.contacts, (item: Contact) => {
291        ListItem() {
292          Row() {
293            Image(item.icon)
294              .width(40)
295              .height(40)
296              .margin(10)
297            Text(item.name).fontSize(20)
298          }
299          .width('100%')
300          .justifyContent(FlexAlign.Start)
301        }
302      }, (item: Contact) => JSON.stringify(item))
303    }
304    .width('100%')
305  }
306}
307```
308
309在List组件中,ForEach除了可以用来循环渲染ListItem,也可以用来循环渲染ListItemGroup。ListItemGroup的循环渲染详细使用请参见[支持分组列表](#支持分组列表)。
310
311
312## 自定义列表样式
313
314
315### 设置内容间距
316
317在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加10vp的间距。
318
319
320```ts
321List({ space: 10 }) {
322  // ...
323}
324```
325
326
327### 添加分隔线
328
329分隔线用来将界面元素隔开,使单个元素更加容易识别。以系统设置场景为例(如下图所示),列表项左侧为图标(如蓝牙图标),右侧为文字描述且分割线在文字下方。
330
331  **图13** 设置列表分隔线样式  
332
333![zh-cn_image_0000001511580960](figures/zh-cn_image_0000001511580960.png)
334
335List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidth和color属性设置分隔线的粗细和颜色。
336
337startMargin和endMargin属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。
338
339
340```ts
341class DividerTmp {
342  strokeWidth: Length = 1;
343  startMargin: Length = 60;
344  endMargin: Length = 10;
345  color: ResourceColor = '#ffe9f0f0';
346
347  constructor(strokeWidth: Length, startMargin: Length, endMargin: Length, color: ResourceColor) {
348    this.strokeWidth = strokeWidth;
349    this.startMargin = startMargin;
350    this.endMargin = endMargin;
351    this.color = color;
352  }
353}
354@Entry
355@Component
356struct EgDivider {
357  @State egDivider: DividerTmp = new DividerTmp(1, 60, 10, '#ffe9f0f0');
358  build() {
359    List() {
360      // ...
361    }
362    .divider(this.egDivider)
363  }
364}
365```
366
367此示例表示从距离列表侧边起始端60vp开始到距离结束端10vp的位置,画一条粗细为1vp的分割线,可以实现图9设置列表分隔线的样式。
368
369>**说明:**
370>
371>1. 分隔线的宽度会使ListItem之间存在一定间隔,当List设置的内容间距小于分隔线宽度时,ListItem之间的间隔会使用分隔线的宽度。
372>
373>2. 当List存在多列时,分割线的startMargin和endMargin作用于每一列上。
374>
375>3. List组件的分隔线画在两个ListItem之间,第一个ListItem上方和最后一个ListItem下方不会绘制分隔线。
376
377
378### 添加滚动条
379
380当列表项高度(宽度)超出屏幕高度(宽度)时,列表可以沿垂直(水平)方向滚动。在页面内容很多时,若用户需快速定位,可拖拽滚动条,如下图所示。
381
382  **图14** 列表的滚动条 
383
384![zh-cn_image_0000001511740544](figures/zh-cn_image_0000001511740544.gif)
385
386在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为[BarState](../reference/apis-arkui/arkui-ts/ts-appendix-enums.md#barstate),当取值为BarState.Auto表示按需显示滚动条。此时,当触摸到滚动条区域时显示控件,可上下拖拽滚动条快速浏览内容,拖拽时会变粗。若不进行任何操作,2秒后滚动条自动消失。
387
388scrollBar属性API version 9及以下版本默认值为BarState.Off,从API version 10版本开始默认值为BarState.Auto389```ts
390List() {
391  // ...
392}
393.scrollBar(BarState.Auto)
394```
395
396## 添加外置滚动条
397
398列表[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)可与[ScrollBar](../reference/apis-arkui/arkui-ts/ts-basic-components-scrollbar.md)组件配合使用,为列表添加外置滚动条。两者通过绑定同一个[Scroller](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scroller)滚动控制器对象实现联动。
399
4001. 首先,需要创建一个[Scroller](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scroller)类型的对象listScroller。
401
402   ```ts
403   private listScroller: Scroller = new Scroller();
404   ```
405
4062. 然后,列表通过[scroller](../reference/apis-arkui/arkui-ts/ts-container-list.md#listoptions18对象说明)参数绑定滚动控制器。
407
408   ```ts
409   // listScroller初始化List组件的scroller参数,绑定listScroller与列表。
410   List({ scroller: this.listScroller }) {
411   // ...
412   }
413   ```
414
4153. 最后,滚动条通过[scroller](../reference/apis-arkui/arkui-ts/ts-basic-components-scrollbar.md#scrollbaroptions对象说明)参数绑定滚动控制器。
416
417   ```ts
418   // listScroller初始化ScrollBar组件的scroller参数,绑定listScroller与列表。
419   ScrollBar({ scroller: this.listScroller })
420   ```
421
422  **图15** 列表的外置滚动条
423
424![ScrollBar](figures/list_scrollbar.gif)
425
426>**说明:**
427>- 滚动条组件[ScrollBar](../reference/apis-arkui/arkui-ts/ts-basic-components-scrollbar.md),还可配合其他可滚动组件使用,如[ArcList](../reference/apis-arkui/arkui-ts/ts-container-arclist.md)、[Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md)、[Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md)、[WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md)。
428>- 在圆形屏幕设备上,[list](../reference/apis-arkui/arkui-ts/ts-container-list.md)可以与弧形滚动条组件[ArcScrollBar](../reference/apis-arkui/arkui-ts/ts-basic-components-arcscrollbar.md)配合使用为列表添加弧形外置滚动条,使用方式可参考[创建弧形列表 (ArcList)](./arkts-layout-development-create-arclist.md)的[添加外置滚动条ArcScrollBar](./arkts-layout-development-create-arclist.md#添加外置滚动条arcscrollbar)章节。
429
430## 支持分组列表
431
432在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见,如下图所示联系人列表。
433
434  **图16** 联系人分组列表 
435
436![zh-cn_image_0000001511580948](figures/zh-cn_image_0000001511580948.png)
437
438在List组件中使用ListItemGroup对项目进行分组,可以构建二维列表。
439
440在List组件中可以直接使用一个或者多个ListItemGroup组件,ListItemGroup的宽度默认充满List组件。在初始化ListItemGroup时,可通过header参数设置列表分组的头部组件。
441
442
443```ts
444@Entry
445@Component
446struct ContactsList {
447
448  @Builder itemHead(text: string) {
449    // 列表分组的头部组件,对应联系人分组A、B等位置的组件
450    Text(text)
451      .fontSize(20)
452      .backgroundColor('#fff1f3f5')
453      .width('100%')
454      .padding(5)
455  }
456
457  build() {
458    List() {
459      ListItemGroup({ header: this.itemHead('A') }) {
460        // 循环渲染分组A的ListItem
461      }
462
463      ListItemGroup({ header: this.itemHead('B') }) {
464        // 循环渲染分组B的ListItem
465      }
466    }
467  }
468}
469```
470
471如果多个ListItemGroup结构类似,可以将多个分组的数据组成数组,然后使用ForEach对多个分组进行循环渲染。例如在联系人列表中,将每个分组的联系人数据contacts(可参考[迭代列表内容](#迭代列表内容)章节)和对应分组的标题title数据进行组合,定义为数组contactsGroups。然后在ForEach中对contactsGroups进行循环渲染,即可实现多个分组的联系人列表。可参考[添加粘性标题](#添加粘性标题)章节示例代码。
472
473## 添加粘性标题
474
475粘性标题是一种常见的标题模式,常用于定位字母列表的头部元素。如下图所示,在联系人列表中滚动A部分时,B部分开始的头部元素始终处于A的下方。而在开始滚动B部分时,B的头部会固定在屏幕顶部,直到所有B的项均完成滚动后,才被后面的头部替代。
476
477粘性标题不仅有助于阐明列表中数据的表示形式和用途,还可以帮助用户在大量信息中进行数据定位,从而避免用户在标题所在的表的顶部与感兴趣区域之间反复滚动。
478
479  **图17** 粘性标题  
480
481![zh-cn_image_0000001511740552](figures/zh-cn_image_0000001511740552.gif)
482
483List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。
484
485通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer486
487
488```ts
489import { util } from '@kit.ArkTS';
490
491class Contact {
492  key: string = util.generateRandomUUID(true);
493  name: string;
494  icon: Resource;
495
496  constructor(name: string, icon: Resource) {
497    this.name = name;
498    this.icon = icon;
499  }
500}
501export class ContactsGroup {
502  title: string = '';
503  contacts: Array<object> | null = null;
504  key: string = "";
505}
506
507export class ContactsGroupDataSource implements IDataSource {
508  private list: object[] = [];
509
510  constructor(list: object[]) {
511    this.list = list;
512  }
513
514  totalCount(): number {
515    return this.list.length;
516  }
517
518  getData(index: number): object {
519    return this.list[index];
520  }
521
522  registerDataChangeListener(listener: DataChangeListener): void {
523  }
524
525  unregisterDataChangeListener(listener: DataChangeListener): void {
526  }
527}
528
529export let contactsGroups: object[] = [
530  {
531    title: 'A',
532    contacts: [
533      new Contact('艾佳', $r('app.media.iconA')),
534      new Contact('安安', $r('app.media.iconB')),
535      new Contact('Angela', $r('app.media.iconC')),
536    ],
537    key: util.generateRandomUUID(true)
538  } as ContactsGroup,
539  {
540    title: 'B',
541    contacts: [
542      new Contact('白叶', $r('app.media.iconD')),
543      new Contact('伯明', $r('app.media.iconE')),
544    ],
545    key: util.generateRandomUUID(true)
546  } as ContactsGroup,
547  // ...
548]
549export let contactsGroupsDataSource: ContactsGroupDataSource = new ContactsGroupDataSource(contactsGroups);
550
551@Entry
552@Component
553struct ContactsList {
554  // 定义分组联系人数据集合contactsGroups数组
555  @Builder itemHead(text: string) {
556    // 列表分组的头部组件,对应联系人分组A、B等位置的组件
557    Text(text)
558      .fontSize(20)
559      .backgroundColor('#fff1f3f5')
560      .width('100%')
561      .padding(5)
562  }
563  build() {
564    List() {
565      // 懒加载ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合
566      LazyForEach(contactsGroupsDataSource, (itemGroup: ContactsGroup) => {
567        ListItemGroup({ header: this.itemHead(itemGroup.title) }) {
568          // 懒加载ListItem
569          if (itemGroup.contacts) {
570            LazyForEach(new ContactsGroupDataSource(itemGroup.contacts), (item: Contact) => {
571              ListItem() {
572                // ...
573              }
574            }, (item: Contact) => JSON.stringify(item))
575          }
576        }
577      }, (itemGroup: ContactsGroup) => JSON.stringify(itemGroup))
578    }.sticky(StickyStyle.Header)  // 设置吸顶,实现粘性标题效果
579  }
580}
581```
582
583
584## 控制滚动位置
585
586控制滚动位置在实际应用中十分常见,例如当新闻页列表项数量庞大,用户滚动列表到一定位置时,希望快速滚动到列表底部或返回列表顶部。此时,可以通过控制滚动位置来实现列表的快速定位,如下图所示。
587
588  **图18** 返回列表顶部  
589
590![zh-cn_image_0000001511900520](figures/zh-cn_image_0000001511900520.gif)
591
592List组件初始化时,可以通过scroller参数绑定一个[Scroller](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scroller)对象,进行列表的滚动控制。例如,用户在新闻应用中,点击新闻页面底部的返回顶部按钮时,就可以通过Scroller对象的scrollToIndex方法使列表滚动到指定的列表项索引位置。
593
594首先,需要创建一个Scroller的对象listScroller。
595
596
597```ts
598private listScroller: Scroller = new Scroller();
599```
600
601然后,通过将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。在需要跳转的位置指定scrollToIndex的参数为0,表示返回列表顶部。
602
603
604```ts
605Stack({ alignContent: Alignment.Bottom }) {
606  // 将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。
607  List({ space: 20, scroller: this.listScroller }) {
608    // ...
609  }
610
611  Button() {
612    // ...
613  }
614  .onClick(() => {
615    // 点击按钮时,指定跳转位置,返回列表顶部
616    this.listScroller.scrollToIndex(0);
617  })
618}
619```
620
621
622## 响应滚动位置
623
624许多应用需要监听列表的滚动位置变化并作出响应。例如,在联系人列表滚动时,如果跨越了不同字母开头的分组,则侧边字母索引栏也需要更新到对应的字母位置。
625
626除了字母索引之外,滚动列表结合多级分类索引在应用开发过程中也很常见,例如购物应用的商品分类页面,多级分类也需要监听列表的滚动位置。
627
628**图19** 字母索引响应联系人列表滚动  
629
630![zh-cn_image_0000001563060769](figures/zh-cn_image_0000001563060769.gif)
631
632如上图所示,当联系人列表从A滚动到B时,右侧索引栏也需要同步从选中A状态变成选中B状态。此场景可以通过监听List组件的onScrollIndex事件来实现,右侧索引栏需要使用字母表索引组件[AlphabetIndexer](../reference/apis-arkui/arkui-ts/ts-container-alphabet-indexer.md)。
633
634在列表滚动时,根据列表此时所在的索引值位置firstIndex,重新计算字母索引栏对应字母的位置selectedIndex。由于AlphabetIndexer组件通过selected属性设置了选中项索引值,当selectedIndex变化时会触发AlphabetIndexer组件重新渲染,从而显示为选中对应字母的状态。
635
636
637```ts
638const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
639  'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
640@Entry
641@Component
642struct ContactsList {
643  @State selectedIndex: number = 0;
644  private listScroller: Scroller = new Scroller();
645
646  build() {
647    Stack({ alignContent: Alignment.End }) {
648      List({ scroller: this.listScroller }) {}
649      .onScrollIndex((firstIndex: number) => {
650        // 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置this.selectedIndex
651      })
652
653      // 字母表索引组件
654      AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
655        .selected(this.selectedIndex)
656        .onSelect((index: number) => {
657          this.listScroller.scrollToIndex(index);
658        })
659    }
660  }
661}
662```
663
664>**说明:**
665>
666>计算索引值时,ListItemGroup作为一个整体占一个索引值,不计算ListItemGroup内部ListItem的索引值。
667
668
669## 响应列表项侧滑
670
671侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息,如下图所示。其中,列表项头像右上角标记设置参考[给列表项添加标记](#给列表项添加标记)。
672
673**图20** 侧滑删除列表项  
674
675![zh-cn_image_0000001563060773](figures/zh-cn_image_0000001563060773.gif)
676
677ListItem的[swipeAction属性](../reference/apis-arkui/arkui-ts/ts-container-listitem.md#swipeaction9)可用于实现列表项的左右滑动功能。swipeAction属性方法初始化时有必填参数SwipeActionOptions,其中,start参数表示设置列表项右滑时起始端滑出的组件,end参数表示设置列表项左滑时尾端滑出的组件。
678
679在消息列表中,end参数表示设置ListItem左滑时尾端划出自定义组件,即删除按钮。在初始化end方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。
680
6811. 实现尾端滑出组件的构建。
682
683    ```ts
684    @Builder itemEnd(index: number) {
685      // 构建尾端滑出组件
686      Button({ type: ButtonType.Circle }) {
687        Image($r('app.media.ic_public_delete_filled'))
688          .width(20)
689          .height(20)
690      }
691      .onClick(() => {
692        // this.messages为列表数据源,可根据实际场景构造。点击后从数据源删除指定数据项。
693        this.messages.splice(index, 1);
694      })
695    }
696    ```
697
6982. 绑定swipeAction属性到可左滑的ListItem上。
699
700    ```ts
701    // 构建List时,通过ForEach基于数据源this.messages循环渲染ListItem。
702    ListItem() {
703      // ...
704    }
705    .swipeAction({
706      end: {
707        // index为该ListItem在List中的索引值。
708        builder: () => { this.itemEnd(index) },
709      }
710    }) // 设置侧滑属性.
711    ```
712
713## 给列表项添加标记
714
715添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息,如下图所示。
716
717  **图21** 给列表项添加标记  
718
719![zh-cn_image_0000001511580952](figures/zh-cn_image_0000001511580952.png)
720
721在ListItem中使用[Badge](../reference/apis-arkui/arkui-ts/ts-container-badge.md)组件可实现给列表项添加标记功能。Badge是可以附加在单个组件上用于信息标记的容器组件。
722
723在消息列表中,若希望在联系人头像右上角添加标记,可在实现消息列表项ListItem的联系人头像时,将头像Image组件作为Badge的子组件。
724
725在Badge组件中,count和position参数用于设置需要展示的消息数量和提示点显示位置,还可以通过style参数灵活设置标记的样式。
726
727
728```ts
729ListItem() {
730  Badge({
731    count: 1,
732    position: BadgePosition.RightTop,
733    style: { badgeSize: 16, badgeColor: '#FA2A2D' }
734  }) {
735    // Image组件实现消息联系人头像
736    // ...
737  }
738}
739```
740
741
742## 下拉刷新与上拉加载
743
744页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md),在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。
745
746以下拉刷新为例,其实现主要分成三步:
747
7481. 监听手指按下事件,记录其初始位置的值。
749
7502. 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。
751
7523. 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。
753
754> **说明:**
755>
756> 页面的下拉刷新操作推荐使用[Refresh](../reference/apis-arkui/arkui-ts/ts-container-refresh.md)组件实现。
757
758<!--RP1--><!--RP1End-->
759
760<!--Del-->
761下拉刷新与上拉加载的具体实现可参考[相关实例](#相关实例)中新闻数据加载。若开发者希望快速实现此功能,也可使用三方组件[PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh)。<!--DelEnd-->
762
763
764## 编辑列表
765
766列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。
767
768下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。
769
770
771### 新增列表项
772
773如下图所示,当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。
774
775  **图22** 新增待办  
776
777![zh-cn_image_0000001511740556](figures/zh-cn_image_0000001511740556.gif)
778
779添加列表项功能实现主要流程如下:
780
7811. 定义列表项数据结构,以待办事项管理为例,首先定义待办数据结构。
782
783   ```ts
784   //ToDo.ets
785   import { util } from '@kit.ArkTS';
786
787   export class ToDo {
788     key: string = util.generateRandomUUID(true);
789     name: string;
790
791     constructor(name: string) {
792       this.name = name;
793     }
794   }
795   ```
796
7972. 构建列表整体布局和列表项。
798
799   ```ts
800   //ToDoListItem.ets
801   import { ToDo } from './ToDo';
802
803   @Component
804   export struct ToDoListItem {
805     @Link isEditMode: boolean;
806     @Link selectedItems: ToDo[];
807     private toDoItem: ToDo = new ToDo("");
808
809     build() {
810      Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
811        // ...
812      }
813      .width('100%')
814      .height(80)
815      //.padding() 根据具体使用场景设置
816      .borderRadius(24)
817      //.linearGradient() 根据具体使用场景设置
818      .gesture(
819        GestureGroup(GestureMode.Exclusive,
820        LongPressGesture()
821          .onAction(() => {
822            // ...
823          })
824        )
825      )
826     }
827   }
828   ```
829
8303. 初始化待办列表数据和可选事项,最后,构建列表布局和列表项。
831
832   ```ts
833   //ToDoList.ets
834   import { ToDo } from './ToDo';
835   import { ToDoListItem } from './ToDoListItem';
836
837   @Entry
838   @Component
839   struct ToDoList {
840     @State toDoData: ToDo[] = [];
841     @Watch('onEditModeChange') @State isEditMode: boolean = false;
842     @State selectedItems: ToDo[] = [];
843    private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌'];
844
845     onEditModeChange() {
846       if (!this.isEditMode) {
847         this.selectedItems = [];
848       }
849    }
850
851     build() {
852       Column() {
853         Row() {
854           if (this.isEditMode) {
855             Text('X')
856               .fontSize(20)
857               .onClick(() => {
858                 this.isEditMode = false;
859               })
860               .margin({ left: 20, right: 20 })
861           } else {
862             Text('待办')
863               .fontSize(36)
864               .margin({ left: 40 })
865             Blank()
866             Text('+') //提供新增列表项入口,即给新增按钮添加点击事件
867               .onClick(() => {
868                 this.getUIContext().showTextPickerDialog({
869                   range: this.availableThings,
870                   onAccept: (value: TextPickerResult) => {
871                     let arr = Array.isArray(value.index) ? value.index : [value.index];
872                     for (let i = 0; i < arr.length; i++) {
873                       this.toDoData.push(new ToDo(this.availableThings[arr[i]])); // 新增列表项数据toDoData(可选事项)
874                     }
875                   },
876                 })
877               })
878           }
879           List({ space: 10 }) {
880             ForEach(this.toDoData, (toDoItem: ToDo) => {
881               ListItem() {
882                 // 将toDoData的每个数据放入到以model的形式放进ListItem里
883                 ToDoListItem({
884                   isEditMode: this.isEditMode,
885                   toDoItem: toDoItem,
886                   selectedItems: this.selectedItems })
887               }
888             }, (toDoItem: ToDo) => toDoItem.key.toString())
889           }
890         }
891       }
892     }
893   }
894   ```
895
896
897### 删除列表项
898
899如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。
900
901  **图23** 长按删除待办事项  
902
903![zh-cn_image_0000001562820877](figures/zh-cn_image_0000001562820877.gif)
904
905删除列表项功能实现主要流程如下:
906
9071. 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。
908   以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。
909
910    ```ts
911    // 结构参考
912    export class ToDo {
913      key: string = util.generateRandomUUID(true);
914      name: string;
915      toDoData: ToDo[] = [];
916
917      constructor(name: string) {
918        this.name = name;
919      }
920    }
921    ```
922    ```ts
923    // 实现参考
924    Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
925      // ...
926    }
927    .gesture(
928    GestureGroup(GestureMode.Exclusive,
929      LongPressGesture()
930        .onAction(() => {
931          if (!this.isEditMode) {
932            this.isEditMode = true; //进入编辑模式
933          }
934        })
935      )
936    )
937    ```
938
9392. 需要响应用户的选择交互,记录要删除的列表项数据。
940   在待办列表中,通过勾选框的勾选或取消勾选,响应用户勾选列表项变化,记录所有选择的列表项。
941
942    ```ts
943   // 结构参考
944   import { util } from '@kit.ArkTS';
945
946   export class ToDo {
947     key: string = util.generateRandomUUID(true);
948     name: string;
949     toDoData: ToDo[] = [];
950
951     constructor(name: string) {
952       this.name = name;
953     }
954   }
955    ```
956    ```ts
957    // 实现参考
958    if (this.isEditMode) {
959      Checkbox()
960        .onChange((isSelected) => {
961          if (isSelected) {
962            this.selectedItems.push(toDoList.toDoItem); // this.selectedItems为勾选时,记录选中的列表项,可根据实际场景构造
963          } else {
964            let index = this.selectedItems.indexOf(toDoList.toDoItem);
965            if (index !== -1) {
966              this.selectedItems.splice(index, 1); // 取消勾选时,则将此项从selectedItems中删除
967            }
968          }
969        })
970    }
971    ```
972
9733. 需要响应用户点击删除按钮事件,删除列表中对应的选项。
974
975    ```ts
976    // 结构参考
977    import { util } from '@kit.ArkTS';
978
979    export class ToDo {
980      key: string = util.generateRandomUUID(true);
981      name: string;
982      toDoData: ToDo[] = [];
983
984      constructor(name: string) {
985        this.name = name;
986      }
987    }
988    ```
989    ```ts
990    // 实现参考
991    Button('删除')
992      .onClick(() => {
993        // this.toDoData为待办的列表项,可根据实际场景构造。点击后删除选中的列表项对应的toDoData数据
994        let leftData = this.toDoData.filter((item) => {
995          return !this.selectedItems.find((selectedItem) => selectedItem == item);
996        })
997        this.toDoData = leftData;
998        this.isEditMode = false;
999      })
1000    ```
1001
1002
1003## 长列表的处理
1004
1005[循环渲染](../ui/state-management/arkts-rendering-control-foreach.md)适用于短列表,当构建具有大量列表项的长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,会导致页面启动时间过长,影响用户体验。因此,推荐使用[数据懒加载](../ui/state-management/arkts-rendering-control-lazyforeach.md)(LazyForEach)方式实现按需迭代加载数据,从而提升列表性能。
1006
1007关于长列表按需加载优化的具体实现可参考[数据懒加载](../ui/state-management/arkts-rendering-control-lazyforeach.md)章节中的示例。
1008
1009当使用懒加载方式渲染列表时,为了更好的列表滚动体验,减少列表滑动时出现白块,List组件提供了cachedCount参数用于设置列表项缓存数,懒加载方式只会预加载List显示区域外cachedCount的内容,而非懒加载会全部加载。无论懒加载还是非懒加载都只布局List显示区域+List显示区域外cachedCount的内容。
1010
1011
1012```ts
1013List() {
1014  // ...
1015}.cachedCount(3)
1016```
1017
1018以垂直列表为例:
1019
1020- List设置cachedCount后,显示区域外上下各会预加载并布局cachedCount行ListItem。计算ListItem行数时,会计算ListItemGroup内部的ListItem行数。如果ListItemGroup内没有ListItem,则整个ListItemGroup算一行。
1021
1022- List下嵌套使用LazyForEach,并且LazyForEach下嵌套使用ListItemGroup时,LazyForEach会在List显示区域外上下各会创建cachedCount个ListItemGroup。
1023
1024>**说明:**
1025>
1026>1. cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。
1027>
1028>2. 列表使用数据懒加载时,除了显示区域的列表项和前后缓存的列表项,其他列表项会被销毁。
1029
1030
1031## 折叠与展开
1032
1033列表项的折叠与展开用途广泛,常用于信息清单的展示、填写等应用场景。
1034
1035  **图24** 列表项的折叠与展开 
1036
1037![zh-cn_image_0000001949866104](figures/zh-cn_image_0000001949866104.gif)
1038
1039列表项折叠与展开效果实现主要流程如下:
1040
10411. 定义列表项数据结构。
1042
1043    ```ts
1044    import { curves } from '@kit.ArkUI';
1045
1046    interface ItemInfo {
1047      index: number,
1048      name: string,
1049      label: ResourceStr,
1050      type?: string,
1051    }
1052
1053    interface ItemGroupInfo extends ItemInfo {
1054      children: ItemInfo[]
1055    }
1056    ```
1057
10582. 构造列表结构。
1059
1060    ```ts
1061    @State routes: ItemGroupInfo[] = [
1062      {
1063        index: 0,
1064        name: 'basicInfo',
1065        label: '个人基本资料',
1066        children: [
1067          {
1068            index: 0,
1069            name: '昵称',
1070            label: 'xxxx',
1071            type: 'Text'
1072          },
1073          {
1074            index: 1,
1075            name: '头像',
1076            label: $r('sys.media.ohos_user_auth_icon_face'),
1077            type: 'Image'
1078          },
1079          {
1080            index: 2,
1081            name: '年龄',
1082            label: 'xxxx',
1083            type: 'Text'
1084          },
1085          {
1086            index: 3,
1087            name: '生日',
1088            label: 'xxxxxxxxx',
1089            type: 'Text'
1090          },
1091          {
1092            index: 4,
1093            name: '性别',
1094            label: 'xxxxxxxx',
1095            type: 'Text'
1096          },
1097        ]
1098      },
1099      {
1100        index: 1,
1101        name: 'equipInfo',
1102        label: '设备信息',
1103        children: []
1104      },
1105      {
1106        index: 2,
1107        name: 'appInfo',
1108        label: '应用使用信息',
1109        children: []
1110      },
1111      {
1112        index: 3,
1113        name: 'uploadInfo',
1114        label: '您主动上传的数据',
1115        children: []
1116      },
1117      {
1118        index: 4,
1119        name: 'tradeInfo',
1120        label: '交易与资产信息',
1121        children: []
1122      },
1123      {
1124        index: 5,
1125        name: 'otherInfo',
1126        label: '其他资料',
1127        children: []
1128      },
1129    ];
1130    @State expandedItems: boolean[] = Array(this.routes.length).fill(false);
1131    @State selection: string | null = null;
1132    build() {
1133      Column() {
1134        // ...
1135
1136        List({ space: 10 }) {
1137          ForEach(this.routes, (itemGroup: ItemGroupInfo) => {
1138            ListItemGroup({
1139              header: this.ListItemGroupHeader(itemGroup),
1140              style: ListItemGroupStyle.CARD,
1141            }) {
1142              if (this.expandedItems[itemGroup.index] && itemGroup.children) {
1143                ForEach(itemGroup.children, (item: ItemInfo) => {
1144                  ListItem({ style: ListItemStyle.CARD }) {
1145                    Row() {
1146                      Text(item.name)
1147                      Blank()
1148                      if (item.type === 'Image') {
1149                        Image(item.label)
1150                          .height(20)
1151                          .width(20)
1152                      } else {
1153                        Text(item.label)
1154                      }
1155                      Image($r('sys.media.ohos_ic_public_arrow_right'))
1156                        .fillColor($r('sys.color.ohos_id_color_fourth'))
1157                        .height(30)
1158                        .width(30)
1159                    }
1160                    .width("100%")
1161                  }
1162                  .width("100%")
1163                  .animation({ curve: curves.interpolatingSpring(0, 1, 528, 39) })
1164                })
1165              }
1166            }.clip(true)
1167          })
1168        }
1169        .width("100%")
1170      }
1171      .width('100%')
1172      .height('100%')
1173      .justifyContent(FlexAlign.Start)
1174      .backgroundColor($r('sys.color.ohos_id_color_sub_background'))
1175    }
1176    ```
1177
11783. 通过改变ListItem的状态,来控制每个列表项是否展开,并通过animation和animateTo来实现展开与折叠过程中的动效效果。
1179
1180    ```ts
1181    @Builder
1182    ListItemGroupHeader(itemGroup: ItemGroupInfo) {
1183      Row() {
1184        Text(itemGroup.label)
1185        Blank()
1186        Image($r('sys.media.ohos_ic_public_arrow_down'))
1187          .fillColor($r('sys.color.ohos_id_color_fourth'))
1188          .height(30)
1189          .width(30)
1190          .rotate({ angle: !!itemGroup.children.length ? (this.expandedItems[itemGroup.index] ? 0 : 180) : 180 })
1191          .animation({ curve: curves.interpolatingSpring(0, 1, 228, 22) })
1192      }
1193      .width("100%")
1194      .padding(10)
1195      .animation({ curve: curves.interpolatingSpring(0, 1, 528, 39) })
1196      .onClick(() => {
1197        if (itemGroup.children.length) {
1198          this.getUIContext()?.animateTo({ curve: curves.interpolatingSpring(0, 1, 528, 39) }, () => {
1199            this.expandedItems[itemGroup.index] = !this.expandedItems[itemGroup.index];
1200          })
1201        }
1202      })
1203    }
1204    ```
1205
1206## 切换布局方向
1207
1208部分业务场景需要列表底部插入数据时,自动向上滚动,把新插入的节点展示出来。例如,直播评论、即时聊天等应用场景。而List组件正常布局时, 在内容下方增加节点,内容是保持不变的。此时,可以通过切换布局方向来实现所需效果。
1209
1210  **图25** 实时消息滚动显示
1211
1212![zh-cn_image_0000001949866105](figures/zh-cn_image_0000001949866105.gif)
1213
12141. 定义列表项数据结构。
1215
1216    ```ts
1217    interface Message {
1218      id: number
1219      content: string
1220      sender: string
1221    }
1222    ```
1223
12242. 构造列表结构,同时把stackFromEnd接口值设置为true,即可实现list列表在底部插入数据时,内容向上滚动。
1225
1226    ```ts
1227    @State messages: Message[] = [
1228        { id: 1, content: '欢迎来到直播间!', sender: '系统' },
1229        { id: 2, content: '大家好啊~', sender: '主播' }
1230    ];
1231    build() {
1232      Column() {
1233        List({ space: 10 }) {
1234          ForEach(this.messages, (item: Message) => {
1235            ListItem() {
1236              this.MessageItem(item)
1237            }
1238          }, (item: Message) => item.id.toString())
1239        }
1240        .stackFromEnd(true)
1241        .layoutWeight(1)
1242        .alignListItem(ListItemAlign.Center)
1243        // ...
1244      }
1245      .width('100%')
1246      .height('100%')
1247    }
1248    ```
1249
1250## 支持滑动离手事件
1251
1252从API version 20开始,滚动类组件([Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md)、[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)、[Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md)、[WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md))支持滑动离手事件回调功能,当用户手指离开屏幕时,会触发该事件并上报离手瞬间的滑动速度。开发者可利用此接口实现类似新闻浏览页面的自定义限位滚动效果,短新闻限位滚动,长新闻自由滚动。
1253
1254  **图26** 自定义限位滚动效果
1255
1256![onWillStopDragging](figures/onWillStopDragging.gif)
1257
12581. 定义新闻条目数据结构。
1259
1260    ```ts
1261    // 结构参考
1262    class news {
1263      public id: string;
1264      public title: string;
1265      public content: string;
1266      public type: string;
1267
1268      constructor(id: string, title: string, content: string, type: string) {
1269        this.id = id;
1270        this.title = title;
1271        this.content = content;
1272        this.type = type;
1273      }
1274    }
1275    ```
1276
12772. 构造新闻条目结构,通过type属性来区分长新闻,短新闻。
1278
1279    ```ts
1280    // 实现参考
1281    @State newsData: Array<news> = [
1282      new news('1', '新闻标题1', '这是第一条短新闻,内容较少,快速滑动切换', 'short'),
1283      new news('2', '新闻标题2', '这是第一条短新闻,内容较少,快速滑动切换', 'short'),
1284      new news('3', '新闻标题3', '这是第二条长新闻,内容较多,可以自由滑动查看完整内容。'.repeat(20), 'long'),
1285      new news('4', '新闻标题4', '这是第三条短新闻,内容较少,快速滑动切换', 'short'),
1286      new news('5', '新闻标题5', '这是第四条长新闻,内容较多,可以自由滑动查看完整内容。', 'long')
1287    ];
1288    ```
1289
12903. 滑动离手事件onWillStopDragging及新闻处理逻辑:
1291   - 上报离手瞬间滑动速度,支持正负方向速度检测,向上滑动为正,向下滑动为负。
1292
1293     ```ts
1294     // 实现参考
1295     onWillStopDragging((velocity: number) => {
1296       if (velocity < 0) {
1297         // 向下滑动处理
1298       } else {
1299         // 向上滑动处理
1300       }
1301     })
1302     ```
1303
1304   - 通过getItemRect接口方法获取当前项位置信息。
1305
1306     ```ts
1307     // 实现参考
1308     let rect = this.scrollerForList.getItemRect(this.currentIndex);
1309     ```
1310
1311   - 处理短新闻:直接跳转相邻项。
1312
1313     ```ts
1314     // 实现参考
1315     if (velocity > 10) {
1316       this.scrollerForList.scrollToIndex(this.currentIndex, true, ScrollAlign.START);
1317     } else if (velocity < -10) {
1318       this.scrollerForList.scrollToIndex(this.currentIndex + 1, true, ScrollAlign.START);
1319     }
1320     ```
1321
1322   - 处理长新闻:计算剩余显示范围决定滚动终点。
1323
1324     ```ts
1325     let rect = this.scrollerForList.getItemRect(this.currentIndex);
1326     if (velocity < -30) {
1327       if (rect) {
1328         // 当前节点在页面内的剩余显示范围
1329         let leftRect = rect.y + rect.height;
1330         //   终点位置
1331         let mainPosition = -velocity * DEFAULT_FRICTION / FRICTION_SCALE;
1332         if (leftRect + mainPosition > 0.75 * this.listHeight) {
1333           this.scrollerForList.scrollToIndex(this.currentIndex + 1, true, ScrollAlign.START);
1334           return;
1335         } else if (leftRect + mainPosition < 0.25 * this.listHeight) {
1336           this.scrollerForList.scrollToIndex(this.currentIndex, true, ScrollAlign.END,
1337             { extraOffset: LengthMetrics.vp(this.listHeight * 0.3) })
1338           return;
1339         }
1340       }
1341     } else if (velocity > 30) {
1342       let leftRect = rect?.y + rect?.height;
1343       let mainPosition = velocity * DEFAULT_FRICTION / FRICTION_SCALE;
1344       if (leftRect + mainPosition > 0.75 * this.listHeight) {
1345         this.scrollerForList.scrollToIndex(this.currentIndex, true, ScrollAlign.START);
1346         return;
1347       }
1348     }
1349     ```
1350
1351## 设置边缘滑动效果
1352
1353边缘滑动效果是指当用户滑动滚动组件至边缘后,继续滑动时触发的交互效果。当前List支持通过[edgeEffect](../reference/apis-arkui/arkui-ts/ts-container-list.md#edgeeffect)属性设置三种边缘滑动效果,分别为弹簧效果(即回弹效果)、阴影效果、无效果。具体效果说明请参见[EdgeEffect](../reference/apis-arkui/arkui-ts/ts-appendix-enums.md#edgeeffect)的枚举说明。
1354
1355当List组件的内容区大于等于一屏时,List的边缘滑动效果默认为回弹效果,如下图所示。
1356
1357  **图27** 边缘回弹效果
1358
1359![edge_effect_spring](figures/edge_effect_spring.gif)
1360
1361设置.edgeEffect(EdgeEffect.None)时,List无边缘滑动效果,如下图所示。
1362
1363  **图28** 无边缘滑动效果
1364
1365![edge_effect_none](figures/edge_effect_none.gif)
1366
1367从API version 18开始,List还支持只设置单边的边缘滑动效果,如设置.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true, effectEdge: EffectEdge.START })来实现起始边有边缘回弹效果,末尾边无效果,如下图所示。
1368
1369  **图29** 单边边缘滑动效果
1370
1371![edge_effect_spring_start](figures/edge_effect_spring_start.gif)
1372
1373需要注意的是,当List组件的内容区小于一屏时,List默认无边缘滑动效果。若要启用边缘回弹效果,可以通过设置.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })来实现。
1374
1375## 相关实例
1376
1377如需详细了解ArkUI中列表的创建与使用,请参考以下示例:
1378
1379- [新闻数据加载](https://gitee.com/openharmony/codelabs/tree/master/NetworkManagement/NewsDataArkTS)
1380
1381- [常用组件和容器低代码开发示例(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/EfficiencyEnhancementKit/SuperVisualSample)
1382
1383- [二级联动(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SecondLevelLinkage)
1384
1385- [List组件的使用之商品列表(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List)
1386
1387- [List组件的使用之设置项(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List_HDC)
1388
1389- [PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh)
1390
1391<!--RP2--><!--RP2End-->
1392