1## 列表上拉加载更多内容 2 3### 场景说明 4 5列表上拉加载更多内容是一种常见的数据更新场景,常见于各类阅读类应用中,例如新闻阅读应用中,当用户浏览新闻列表到底部时,上拉将加载更多的新闻内容。本例将介绍列表上拉加载更多内容这个场景的具体实现方式。 6 7### 效果呈现 8 9本示例最终效果如下: 10 11 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.UP或TouchType.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```