实现List下拉刷新和上拉加载更多
=================

#### 问题现象

在List组件中，如何实现下拉刷新和上拉加载的功能？

#### 背景知识

*   [PullToRefresh](https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Fpulltorefresh)是一款OpenHarmony环境下可用的下拉刷新、上拉加载组件。支持设置内置动画的各种属性，支持设置自定义动画，支持lazyForEach的数据作为数据源。
    
    PullToRefresh使用限制：
    
    1.  目前只支持List、Scroll、Tabs、Grid和WaterFlow系统容器组件。
    2.  暂不支持设置系统容器组件的弹簧效果和阴影效果，使用时需要将系统组件edgeEffect属性的值设置为(EdgeEffect.None)。
    3.  暂不支持页面触底时自动触发上拉加载功能。
    4.  暂不支持在页面数据不满一屏时触发上拉加载功能。
    5.  暂不支持通过代码的方式去触发下拉刷新功能。
    6.  暂不支持在下拉刷新动画结束时提供手势结束的回调。
*   [Refresh：](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-refresh)可以进行页面下拉操作并显示刷新动效的容器组件。
*   [List组件：](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list)列表包含一系列相同宽度的列表项。
*   [Button组件：](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-common-components-button)按钮组件通常用于响应用户的点击操作，其类型包括胶囊按钮、圆形按钮、普通按钮、圆角矩形按钮。Button作为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。
*   [$$运算符：](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-two-way-sync)$$运算符为系统组件提供TS变量的引用，使得TS变量和系统组件的内部状态保持同步。

#### 解决方案

*   **场景一**：实现List滚动吸顶并下拉刷新，且下拉刷新的位置在吸顶的下方，上拉可以加载更多。
    
    在目录“entry/src/oh-package.json5”文件中配置PullToRefresh依赖，本示例PullToRefresh三方库版本为2.1.2：
    
    {
      "name": "entry",
      "version": "1.0.0",
      "description": "Please describe the basic information.",
      "main": "",
      "author": "",
      "license": "",
      "dependencies": {
        "@ohos/pulltorefresh": "^2.1.2"
      }
    }
    
    完整示例参考如下：
    
    import { PullToRefresh } from '@ohos/pulltorefresh';
    
    @Entry
    @Component
    struct ListStickyHeaderOne {
      scroller: Scroller = new Scroller();
      private tabscroller: Scroller = new Scroller();
      @State itemData: Array<number> = \[0, 1, 2, 3, 4, 5, 6, 7, 8, 9\];
      tabTitles: Array<string> = \['Tab1', 'Tab2', 'Tab3'\];
    
      @Builder
      private getListView() {
        List({ scroller: this.tabscroller }) {
          ForEach(this.itemData, (item: number) => {
            ListItem() {
              Text(\`${item}\`)
                .height(100)
                .width('100%')
                .borderRadius(10)
                .textAlign(TextAlign.Center)
                .backgroundColor('#f1f3f5')
                .margin({ bottom: 16 });
            };
          });
        }
        .nestedScroll({
          scrollForward: NestedScrollMode.PARENT\_FIRST,
          scrollBackward: NestedScrollMode.SELF\_FIRST
        })
        .backgroundColor('#ffffffff')
      _  // .divider({ strokeWidth: 1, color: 0x222222 })_
        .edgeEffect(EdgeEffect.None); _// 必须设置列表为滑动到边缘无效果_
      }
    
      @Builder
      tabContentData(tabTitle: string) {
        TabContent() {
          Column() {
            PullToRefresh({
              data: this.itemData,
              scroller: this.tabscroller,
              customList: () => {
                this.getListView();
              },
           _   // 可选项，下拉刷新回调_
              onRefresh: () => {
                return new Promise<string>((resolve) => {
                  _// 模拟网络请求操作，请求网络2秒后得到数据，通知组件，变更列表数据_
                  setTimeout(() => {
                    resolve('刷新成功');
                    let num = this.itemData.length;
                    this.itemData.push(num);
                  }, 500);
                });
              },
            _  // 可选项，上拉加载更多回调_
              onLoadMore: () => {
                return new Promise<string>((resolve) => {
                  setTimeout(() => {
                    resolve('');
                    let num = this.itemData.length;
                    this.itemData.push(num);
                  }, 2000);
                });
              },
              customLoad: null,
              customRefresh: null,
            });
          };
        }
        .tabBar(tabTitle)
        .padding({ top: 5, bottom: 5 });
      }
    
      build() {
        Column({ space: 10 }) {
          Scroll(this.scroller) {
            Column() {
              Tabs() {
                ForEach(this.tabTitles, (title: string) => {
                  this.tabContentData(title);
                });
              };
            }.width('92%').alignItems(HorizontalAlign.Center);
          }.width('100%').align(Alignment.Center).scrollBar(BarState.Off);
        }.backgroundColor('#ffffffff')
        .expandSafeArea(\[SafeAreaType.SYSTEM\], \[SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP\]);
      }
    }
    
*   **场景二**：通过点击按钮实现List列表下拉刷新。
    
    可以使用[$$](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-two-way-sync)双向绑定Refresh组件参数refreshing，点击按钮手动控制组件刷新状态实现。
    
    @Entry
    @Component
    struct RefreshExample {
      @State isRefreshing: boolean = false;
      @State arr: String\[\] = \['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'\];
    
      build() {
        Column() {
          Row() {
            Button('开始刷新')
              .margin(16)
              .onClick(() => {
            _    // 手动开始刷新_
                this.isRefreshing = true;
              });
            Button('结束刷新')
              .margin(16)
              .onClick(() => {
            _    // 手动停止刷新_
                this.isRefreshing = false;
              });
          };
    
          Refresh({ refreshing: $$this.isRefreshing }) {
            List() {
              ForEach(this.arr, (item: string) => {
                ListItem() {
                  Text('' + item)
                    .width('92%')
                    .height(100)
                    .textAlign(TextAlign.Center)
                    .fontSize(16)
                    .margin({ bottom:16 ,left:16,right:16})
                    .borderRadius(10)
                    .backgroundColor('#f1f3f5');
                };
              }, (item: string) => item);
            };
          }
          .onRefreshing(() => {
        _    // 2秒后自动结束刷新_
            setTimeout(() => {
              this.isRefreshing = false;
            }, 2000);
          })
          .backgroundColor('#ffffffff');
        }.backgroundColor('#ffffffff').expandSafeArea(\[SafeAreaType.SYSTEM\], \[SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP\]);
      }
    }
    
*   **场景三**：在List嵌套List场景下实现外层List的下拉刷新，内层List的上拉加载更多。
    
    可以在外层List使用Refresh组件实现上拉刷新，内层List使用PullToRefresh进行下拉加载，PullToRefresh依赖配置同场景一。
    
    import { PullToRefresh, PullToRefreshConfigurator } from '@ohos/pulltorefresh';
    
    const tabsList = \['能源', '化工', '塑料', '化纤', '聚氨酯'\];
    
    @Component
    struct Index {
      private refreshScroller: Scroller = new Scroller();
      private refreshConfigurator: PullToRefreshConfigurator = new PullToRefreshConfigurator();
      @State list: Array<string> =
        \['a', 'b', 'c', 'd', 'e', 'f', 'a1', 'b1', 'c1', 'd1', 'e1', 'f1'\];
    
      aboutToAppear(): void {
        this.refreshConfigurator.setHasRefresh(false);
      }
    
      @Builder
      getListView() {
        List({ space: 10, scroller: this.refreshScroller }) {
          ForEach(this.list,
            (item: string) => {
              ListItem() {
                Text(item)
                  .height(100)
                  .width('92%')
                  .borderRadius(10)
                  .textAlign(TextAlign.Center)
                  .margin({ right: 16, left: 16, bottom: 16 })
                  .backgroundColor('#F1F3F5');
              };
            });
        }
        .height('100%')
        .width('100%')
        .scrollBar(BarState.Off)
        .edgeEffect(EdgeEffect.None)
        .nestedScroll({
          scrollForward: NestedScrollMode.PARENT\_FIRST,
          scrollBackward: NestedScrollMode.SELF\_FIRST
        });
      }
    
      build() {
        Column({ space: 10 }) {
          PullToRefresh({
          _  // 必传项，列表组件所绑定的数据_
            data: $list,
            refreshConfigurator: this.refreshConfigurator,
         _   // 必传项，需绑定传入主体布局内的列表或宫格组件_
            scroller: this.refreshScroller,
           _ // 必传项，自定义主体布局，内部有列表或宫格组件_
            customList: () => {
         _     // 一个用@Builder修饰过的UI方法_
              this.getListView();
            },
         _   // 可选项，上拉加载更多回调_
            onLoadMore: () => {
              return new Promise<string>((resolve) => {
            _    // 模拟网络请求操作，请求网络2秒后得到数据，通知组件，变更列表数据_
                setTimeout(() => {
                  resolve('加载更多');
                  this.list.push('a2');
                  this.list.push('b2');
                }, 2000);
              });
            },
            customLoad: null,
            customRefresh: null,
          }).width('100%').height('100%');
        }.width('100%');
      }
    }
    
    @Entry
    @Component
    struct KeyboadPage {
      private refreshScroller: Scroller = new Scroller();
      @State isRefresh: boolean = false;
    
      build() {
        Column({ space: 10 }) {
          Refresh({ refreshing: $$this.isRefresh, builder: this.buildRefreshCom() }) {
            this.getListView();
          }.onRefreshing(() => {
            setTimeout(() => {
              this.isRefresh = false;
            }, 1000);
          });
        }
        .width('100%')
        .height('100%');
      }
    
      @Builder
      buildRefreshCom() {
        Row() {
          LoadingProgress().height(32);
          Text('正在刷新...').fontSize(16).margin({
            left: 20,
          });
        }
        .alignItems(VerticalAlign.Center);
      }
    
      @Builder
      getListView() {
        List({ scroller: this.refreshScroller }) {
          ListItem() {
            Text('第一条')
              .textAlign(TextAlign.Center)
              .width('92%')
              .margin({ right: 16, left: 16, bottom: 16 })
              .height(100)
              .borderRadius(10)
              .backgroundColor('#F1F3F5');
          };
    
          ListItem() {
            Text('第二条')
              .textAlign(TextAlign.Center)
              .width('92%')
              .margin({ right: 16, left: 16, bottom: 16 })
              .height(100)
              .borderRadius(10)
              .backgroundColor('#F1F3F5');
          };
    
          ListItem() {
            Text('第三条')
              .textAlign(TextAlign.Center)
              .width('92%')
              .margin({ right: 16, left: 16, bottom: 16 })
              .height(100)
              .borderRadius(10)
              .backgroundColor('#F1F3F5');
          };
    
          ListItem() {
            Tabs({
              barPosition: BarPosition.Start,
            }) {
              ForEach(tabsList,
                (item: string, idx: number) => {
                  TabContent() {
                    Index();
                  }
                  .tabBar(this.watchingTabBuilder(idx, item));
                });
            }
            .width('100%')
            .barHeight(50)
            .barMode(BarMode.Scrollable);
          };
        }
        .edgeEffect(EdgeEffect.None)
        .height('100%')
        .width('100%')
        .scrollBar(BarState.Off);
      }
    
      @Builder
      watchingTabBuilder(index: number, item: string) {
        Text(item)
          .fontSize(16)
          .fontColor('#121212')
          .padding({
            left: 15,
            right: 15,
            top: 12,
            bottom: 12
          })
          .borderRadius(5);
      }
    }
    

#### 常见FAQ

Q：页面的数据已经全部加载完成时，如何结束上拉加载的状态？

A：将[PullToRefreshConfigurator类](https://gitee.com/openharmony-sig/ohos_pull_to_refresh#pulltorefreshconfigurator类接口)的setHasLoadMore属性为false，即关闭上拉加载功能。

Q：除了使用PullToRefresh三方组件，是否还有其他方式实现List下拉刷新？

A：容器组件Refresh组件能够进行页面下拉操作并显示刷新动效。