1## 列表上拉加载更多内容
2
3### 场景说明
4
5列表上拉加载更多内容是一种常见的数据更新场景,常见于各类阅读类应用中,例如新闻阅读应用中,当用户浏览新闻列表到底部时,上拉将加载更多的新闻内容。本例将介绍列表上拉加载更多内容这个场景的具体实现方式。
6
7### 效果呈现
8
9本示例最终效果如下:
10
11![list-pullup-loading](figures/list-pullup-loading.gif)
12
13### 运行环境
14
15本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发:
16
17- IDE:DevEco Studio 3.1 Release
18- SDK:Ohos_sdk_public 3.2.12.2 (API Version 9 Release)
19
20### 实现思路
21
221. 在页面布局上,通过在List列表末尾手工增加一个ListItem用于显示加载更多布局,通过visibility属性控制该ListItem的显示和隐藏,当上拉时显示,并根据加载中以及加载结果显示不同内容,加载完成后则隐藏。
232. 监听List列表的onTouch事件,并判断是否为上划、滑动距离是否达到设定的阈值等条件,在条件满足后,触发数据加载逻辑,重新渲染列表。
24
25### 开发步骤
26
271. 利用ForEach循环渲染初始列表,在列表末尾增加一个ListItem用于显示加载更多布局,通过visibility属性控制显示和隐藏,并在List列表上拉过程中监听并处理onTouch事件。
28
29   末尾的ListItem可呈现的内容有:
30
31   * 正在加载中显示“正在加载”。
32
33   * 加载失败则显示"加载失败,点击重试",引导用户进行点击,从而重新加载。
34
35   ```ts
36   List({ space: 20, initialIndex: 0 }) {
37       ForEach(this.list, (item) => {
38         ListItem() {
39           ...
40         }
41       }, item => item.toString())
42
43       ListItem() { // 加载更多布局
44         Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
45           if (this.isShowRetry) {
46             Text('加载失败,点击重试')
47               ...
48           } else {
49             ...
50             LoadingProgress()
51             Text('正在加载...')
52           }
53         }
54
55       }.visibility(this.isLoadMore ? Visibility.Visible : Visibility.None)
56   }
57   .onTouch(event => this.handleTouchEvent(event))
58   ```
59
602. 实现列表上拉加载更多内容的核心,是设计处理onTouch事件的响应函数handleTouchEvent,在该函数中需要根据手指滑动的类型TouchType进行不同的处理:
61
62   a. 手指按下时(TouchType.Down),记录按下的坐标,用于后续滑动方向及滑动距离计算:
63
64   ```ts
65   case TouchType.Down: // 手指按下
66       this.downY = event.touches[0].y; // 记录按下的y坐标
67       this.lastMoveY = event.touches[0].y;
68       break;
69   ```
70
71   b. 手指滑动时(TouchType.Move),首先判断是否为上拉动作,如果为上拉动作,再判断是否达到上拉加载更多内容的条件。条件包括:
72
73   * 是否有更多数据正在处于加载中状态,避免同时加载更多数据,只有当前未处于数据加载状态时才可以加载更多数据,否则直接忽略本次上拉动作。
74
75   * 是否当前列表已滑动到最后一项,只有在最后一项时才可以加载更多数据。
76
77   * 上拉滑动的偏移量是否达到设定的阈值,只有达到阈值才可以加载更多数据。
78
79   ```ts
80   case TouchType.Move: // 手指滑动
81       if (this.isLoading) { // 更多数据加载中,不进入处理逻辑
82         return;
83       }
84       if (event.touches[0].y - this.lastMoveY < 0) { // 手指上滑
85         // 因为加载更多是在列表后面新增一个item,当一屏能够展示全部列表,endIndex 为 length+1
86         if (this.endIndex == this.list.length - 1 || this.endIndex == this.list.length) {
87           this.offsetY = event.touches[0].y - this.downY; // 滑动的偏移量
88           if (Math.abs(this.offsetY) > this.loadMoreHeight) { // 数据加载的阈值
89             this.isLoadMore = true // 可以刷新了
90             this.offsetY = this.loadMoreHeight + this.offsetY * 0.1 // 偏移量缓慢增加
91           }
92         }
93       }
94       this.lastMoveY = event.touches[0].y;
95       break;
96   ```
97
98   c. 手指抬起或滑动取消时(TouchType.UPTouchType.Cancel),如果正处于数据加载中状态,则忽略,否则加载更多数据。
99
100   ```ts
101   case TouchType.Up:// 手指抬起
102
103   case TouchType.Cancel:// 事件取消
104   	if (this.isLoading) { // 更多数据加载中,不进入处理逻辑
105   	  return;
106   	}
107   	this.touchUpLoadMore()
108   	break
109   ```
110
1113. 实现加载更多数据的逻辑,即在上述步骤的touchUpLoadMore方法中实现以下处理逻辑:
112
113   a. 通过动画处理列表回弹效果。
114
115   ```ts
116   animateTo({
117     duration: 300, // 动画时长
118   }, () => {
119     this.offsetY = 0 // 偏移量设置为0
120   })
121   ```
122
123   b. 模拟请求加载更多数据的结果,包括:
124   - 正在加载:显示加载中效果。
125
126   - 加载成功:重新渲染List,更新加载状态。
127
128   - 加载失败:展示重新加载的引导说明,点击后重新触发touchUpLoadMore方法。
129
130   ```ts
131   if (this.isLoadMore) {
132     this.isLoading = true // 加载中...
133     setTimeout(() => { // 模拟耗时操作
134   	this.getData()
135   	  .then(data => {
136   		if (data === StatusType.SUCCESS) { // 加载成功
137   		  this.isShowRetry = false
138   		  this.loadMoreData() // 加载数据
139   		  this.isLoadMore = false // 关闭加载更多
140   		  this.isLoading = false
141   		}
142   	  })
143   	  .catch(error => { // 加载失败
144   		this.isShowRetry = true // 展示“点击重试”
145   		console.info('error message ' + error)
146   	  })
147     }, 2000)
148   } else { // 关闭加载更多
149     this.isLoadMore = false
150     this.isLoading = false
151   }
152   ```
153
154
155
156### 完整代码
157
158通过上述步骤可以完成整个示例的开发,完整代码如下:
159
160```ts
161@Entry
162@Component
163struct ListPullupLoading {
164  @State list: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
165  @State offsetY: number = 0 // 列表y坐标偏移量
166  private downY: number = 0 // 按下的y坐标
167  private lastMoveY: number = 0 // 上一次移动的坐标
168  private endIndex: number = 0 // 当前列表尾部索引
169  private loadMoreHeight = 100 // 触发上拉加载的阈值高度
170
171  @State isLoadMore: boolean = false // 是否可以加载更多,上拉加载的布局是否显示
172  private  isLoading: boolean = false // 是否加载中,加载中不进入触摸逻辑
173  @State isShowRetry: boolean = false // 点击重试 是否显示
174
175  build() {
176    Column() {
177      List({ space: 20, initialIndex: 0 }) {
178        ForEach(this.list, (item) => {
179          ListItem() {
180            Text('' + item).width('100%').height('100%')
181              .fontSize(24)
182              .textAlign(TextAlign.Center)
183              .borderRadius(10)
184              .backgroundColor(0xDCDCDC)
185          }.width('100%').height(100)
186        }, item => item.toString())
187
188        ListItem() { // 加载更多布局
189          Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
190            if (this.isShowRetry) {
191              Text('加载失败,点击重试')
192                .margin({ left: 7, bottom: 1 })
193                .fontColor(Color.Grey)
194                .fontSize(24)
195                .onClick(() => {
196                  this.isShowRetry = false
197                  this.touchUpLoadMore()
198                })
199            } else {
200              LoadingProgress()
201                .width(36).height(36)
202
203              Text('正在加载...')
204                .margin({ left: 7, bottom: 1 })
205                .fontColor(Color.Grey)
206                .fontSize(24)
207            }
208          }.width('100%').height('100%')
209          .backgroundColor(0xFFFFFF)
210
211        }
212        .height(this.loadMoreHeight)
213        .visibility(this.isLoadMore ? Visibility.Visible : Visibility.None)
214
215      }
216      .width('100%')
217      .height('100%')
218      .listDirection(Axis.Vertical) // 排列方向
219      .onScrollIndex((start: number, end: number) => {
220        console.info('start = ' + start.toString() + ' end = ' + end.toString())
221        this.endIndex = end
222      })
223      .onTouch(event => this.handleTouchEvent(event))
224    }
225    .width('100%')
226    .height('100%')
227    .backgroundColor(0xFFFFFF)
228  }
229
230  /**
231   * 处理onTouch事件
232   */
233  handleTouchEvent(event: TouchEvent) {
234    switch (event.type) {
235      case TouchType.Down: // 手指按下
236        this.downY = event.touches[0].y; // 记录按下的y坐标
237        this.lastMoveY = event.touches[0].y;
238        break;
239
240      case TouchType.Move: // 手指滑动
241        if (this.isLoading) { // 更多数据加载中,不进入处理逻辑
242          return;
243        }
244        if (event.touches[0].y - this.lastMoveY < 0) { // 手指上滑
245          // 因为加载更多是在列表后面新增一个item,当一屏能够展示全部列表,endIndex 为 length+1
246          if (this.endIndex == this.list.length - 1 || this.endIndex == this.list.length) {
247            this.offsetY = event.touches[0].y - this.downY; // 滑动的偏移量
248            if (Math.abs(this.offsetY) > this.loadMoreHeight) { // 数据加载的阈值
249              this.isLoadMore = true // 可以刷新了
250              this.offsetY = this.loadMoreHeight + this.offsetY * 0.1 // 偏移量缓慢增加
251            }
252          }
253        }
254        this.lastMoveY = event.touches[0].y;
255        break;
256
257      case TouchType.Up:// 手指抬起
258
259      case TouchType.Cancel:// 事件取消
260        if (this.isLoading) { // 更多数据加载中,不进入处理逻辑
261          return;
262        }
263        this.touchUpLoadMore()
264        break
265    }
266  }
267
268  /**
269   * 手指抬起,处理加载更多
270   */
271  private touchUpLoadMore() {
272    animateTo({
273      duration: 300, // 动画时长
274    }, () => {
275      this.offsetY = 0 // 偏移量设置为0
276    })
277
278    if (this.isLoadMore) {
279      this.isLoading = true // 加载中...
280      setTimeout(() => { // 模拟耗时操作
281        this.getData()
282          .then(data => {
283            if (data === StatusType.SUCCESS) { // 加载成功
284              this.isShowRetry = false
285              this.loadMoreData() // 加载数据
286              this.isLoadMore = false // 关闭加载更多
287              this.isLoading = false
288            }
289          })
290          .catch(error => { // 加载失败
291            this.isShowRetry = true // 展示“点击重试”
292            console.info('error message ' + error)
293          })
294      }, 2000)
295    } else { // 关闭加载更多
296      this.isLoadMore = false
297      this.isLoading = false
298    }
299  }
300
301  /**
302   * mock 产生更多数据
303   */
304  private loadMoreData() {
305    let initValue = this.list[this.list.length - 1] + 1;
306    for (let i = initValue; i < initValue + 10; i++) {
307      this.list.push(i)
308    }
309  }
310
311  /**
312   * 模拟数据加载结果
313   */
314  private getData(): Promise<StatusType> {
315    return new Promise((resolve, reject) => {
316      const randomNumber: number = Math.random();
317      if (randomNumber >= 0.5) {
318        resolve(StatusType.SUCCESS)
319      } else {
320        reject(StatusType.FAIL)
321      }
322    })
323  }
324}
325
326enum StatusType {
327  SUCCESS,
328  FAIL
329}
330```