自定义组件之<九>自定义下拉刷新上拉加载(RefreshLayout)

2024-12-31 10:27:24
29次阅读
0个评论

9.8:自定义下拉刷新上拉加载(RefreshLayout)

自定义完毕下拉刷新后接着是给其实现上拉加载,实现顺序和下拉刷新相反的,运行效果如下图所示:

demo.gif

9.8.1:RefreshLayout完整样例

下拉刷新和上拉加载的实现是一致的,笔者不再详述实现过程了,源码如下所示:

export class Constant {
  static readonly REFRESH_PULL_TO_REFRESH   = "下拉刷新";
  static readonly REFRESH_FREE_TO_REFRESH   = "释放立即刷新";
  static readonly REFRESH_REFRESHING        = "正在刷新";
  static readonly REFRESH_SUCCESS           = "刷新成功";
  static readonly REFRESH_PULL_TO_LOAD_MORE = "上拉加载更多";
}

@ComponentV2
export struct RefreshLayout {

  @BuilderParam itemLayout?: (item: Object, index: number) => void;

  @Param refreshing: boolean = false
  @Event $refreshing: (refreshing: boolean) => void = (refreshing: boolean) => {}
  @Param loadMore: boolean = false
  @Event $loadMore: (loadMore: boolean) => void = (loadMore: boolean) => {}
  @Param dataSet: Array<Object> = new Array<Object>()

  @Param onRefresh: () => void = () => {}
  @Param onLoadMore: () => void = () => {}
  @Param onStatusChanged: (status: RefreshStatus) => void = (status: RefreshStatus) => {}

  private headHeight: number = 55;
  private footHeight: number = 55;
  private lastX: number = 0;
  private lastY: number = 0;
  private downY: number = 0;
  private flingFactor: number = 0.75;
  private touchSlop: number = 2;
  private offsetStep: number = 10;
  private intervalTime: number = 20;
  private listScrollable: boolean = true;
  private dragging: boolean = false;
  private refreshStatus: RefreshStatus = RefreshStatus.Inactive;
  private listScroller: Scroller = new Scroller();

  @Local offsetY: number = -this.headHeight;
  @Local refreshHeadIcon: Resource = $r("app.media.icon_refresh_down");
  @Local refreshFootIcon: Resource = $r("app.media.icon_refresh_up");
  @Local refreshFootText: string = Constant.REFRESH_PULL_TO_LOAD_MORE;
  @Local refreshHeadText: string = Constant.REFRESH_PULL_TO_REFRESH;
  @Local refreshContentH: number = 0;
  @Local touchEnabled: boolean = true;



  @Monitor("refreshing")
  notifyRefreshingChanged(monitor: IMonitor) {
    if (this.refreshing) {
      this.showRefreshingStatus();
    } else {
      this.finishRefresh();
    }
  }

  @Monitor("loadMore")
  notifyLoadMoreChanged(monitor: IMonitor) {
    if (this.loadMore) {
      this.showLoadMoreStatus();
    } else {
      this.finishLoadMore();
    }
  }

  @Builder headLayout() {
    Row() {
      Blank()
      Image(this.refreshHeadIcon)
        .width(30)
        .aspectRatio(1)
        .objectFit(ImageFit.Contain)

      Text(this.refreshHeadText)
        .fontSize(16)
        .width(150)
        .textAlign(TextAlign.Center)

      Blank()
    }
    .width("100%")
    .height(this.headHeight)
    .backgroundColor("#44bbccaa")
    .position({
      x: 0,
      y: this.offsetY
    })
  }

  @Builder footLayout() {
    Row() {
      Blank()

      Image(this.refreshFootIcon)
        .width(30)
        .aspectRatio(1)
        .objectFit(ImageFit.Contain)

      Text(this.refreshFootText)
        .fontSize(16)
        .width(150)
        .textAlign(TextAlign.Center)

      Blank()
    }
    .width("100%")
    .height(this.footHeight)
    .backgroundColor("#33bbccaa")
    .position({
      x: 0,
      y: this.offsetY + this.headHeight + this.refreshContentH
    })
  }

  build() {
    Column() {
      this.headLayout()

      List({ space: 20, initialIndex: 0 , scroller: this.listScroller}) {
        if (this.dataSet) {
          ForEach(this.dataSet, (item: Object, index: number) => {
            ListItem() {
              if (this.itemLayout) {
                this.itemLayout(item, index)
              }
            }
            .width("100%")
          }, (item: Object, index: number) => item.toString())
        }
      }
      .width("100%")
      .height(this.refreshContentH)
      .edgeEffect(EdgeEffect.None)
      .enabled(this.touchEnabled)
      .position({
        x: 0,
        y: this.offsetY + this.headHeight
      })
      .onScrollFrameBegin((offset: number, state: ScrollState) => {
        offset = this.listScrollable ? offset : 0;
        return {offsetRemain: offset}
      })
      this.footLayout()
    }
    .width("100%")
    .height("100%")
    .enabled(this.touchEnabled)
    .onAreaChange((oldArea, newAre) => {
      this.logD("刷新内容高度:" + newAre.height)
      this.refreshContentH = newAre.height as number;
    })
    .clip(true)
    .onTouch((event) => {
      if (event.touches.length != 1) {
        this.logD("TOUCHES LENGTH INVALID: " + JSON.stringify(event.touches))
        return;
      }
      if (this.refreshStatus == RefreshStatus.Refresh || this.refreshStatus == RefreshStatus.Done) {
        this.logD("REFRESH STATUS: " + this.refreshStatus);
        return;
      }
      switch (event.type) {
        case TouchType.Down:
          this.onTouchDown(event);
          break;
        case TouchType.Move:
          this.onTouchMove(event);
          break;
        case TouchType.Up:
        case TouchType.Cancel:
          this.onTouchUp(event);
          break;
      }
    })
  }

  aboutToAppear() {
    if (this.refreshing) {
      this.showRefreshingStatus();
    }
  }

  private setRefreshStatus(status: RefreshStatus) {
    this.refreshStatus = status;
    this.touchEnabled = (status != RefreshStatus.Refresh && status != RefreshStatus.Done);
    this.notifyStatusChanged();
  }

  private onTouchDown(event: TouchEvent) {
    this.lastX = event.touches[0].x;
    this.lastY = event.touches[0].y;
    this.downY = this.lastY;
    this.dragging = false;
    this.listScrollable = true;

    // this.logD("Touch DOWN: " + event.touches[0].x.toFixed(2) + " x " + event.touches[0].y.toFixed(2) + ", offset: " + this.offsetY);
  }

  private canPullToRefresh(): boolean {
    return this.listScroller.currentOffset().yOffset == 0;
  }

  private canLoadToRefresh(): boolean {
    return this.listScroller.isAtEnd();
  }

  private onTouchMove(event: TouchEvent) {
    let currentX = event.touches[0].x;
    let currentY = event.touches[0].y;
    let deltaX = currentX - this.lastX;
    let deltaY = currentY - this.lastY;
    let canRefresh = this.canPullToRefresh()
    let canLoadMore = this.canLoadToRefresh()

    if (canRefresh || canLoadMore) {
      if (this.dragging) {
        this.logD("offsetY: " + this.offsetY + ",  head: " + (-this.headHeight));
        if (deltaY < 0) {
          // 手指向上滑动
          if (canLoadMore) {
            this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
            this.lastX = currentX;
            this.lastY = currentY;
          } else {
            if (this.offsetY > -this.headHeight) {
              // this.logD("手指向上拖动还未到达临界值,不让 list 滚动")
              this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
              this.lastX = currentX;
              this.lastY = currentY;
              this.listScrollable = false;
            } else {
              // this.logD("手指向上拖动到达临界值了,开始让 list 滚动")
              this.offsetY = -this.headHeight;
              this.listScrollable = true;
              this.lastX = currentX;
              this.lastY = currentY;
              this.downY = this.lastY;
            }
          }
        } else {
          // 手指向下滑动
          if (canLoadMore) {
            if (this.offsetY < -this.headHeight) {
              // this.logD("手指向下拖动还未到达临界值,不让 list 滚动")
              this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
              this.lastX = currentX;
              this.lastY = currentY;
              this.listScrollable = false;
            } else {
              // this.logD("手指向下拖动到达临界值了,开始让 list 滚动")
              this.offsetY = -this.headHeight;
              this.listScrollable = true;
              this.dragging = false;
              this.lastX = currentX;
              this.lastY = currentY;
              this.downY = this.lastY;
            }
          } else {
            if (this.listScroller.currentOffset().yOffset == 0) {
              this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
            }
            this.lastX = currentX;
            this.lastY = currentY;
          }
        }
      } else {
        if (Math.abs(deltaX) < Math.abs(deltaY) && Math.abs(deltaY) > this.touchSlop) {
          if (deltaY > 0 && canRefresh) {
            this.dragging = true;
            this.listScrollable = false
            this.lastX = currentX;
            this.lastY = currentY;
            this.logD("Touch MOVE: 手指向下滑动,达到了拖动条件")
          }
          if (deltaY < 0 && canLoadMore) {
            this.dragging = true;
            this.listScrollable = false;
            this.lastX = currentX;
            this.lastY = currentY;
            this.logD("Touch MOVE: 手指向上滑动,达到了拖动条件")
          }
        }
      }
    }

    if(this.dragging) {
      if (currentY >= this.downY) {
        if (this.offsetY >= 0 || (this.headHeight - Math.abs(this.offsetY)) > this.headHeight * 4 / 5) {
          this.refreshHeadText = Constant.REFRESH_FREE_TO_REFRESH;
          this.refreshHeadIcon = $r("app.media.icon_refresh_up");
          this.setRefreshStatus(RefreshStatus.OverDrag);
        } else {
          this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;
          this.refreshHeadIcon = $r("app.media.icon_refresh_down");
          this.setRefreshStatus(RefreshStatus.Drag);
        }
      } else {
        if ((Math.abs(this.offsetY) - this.footHeight) > this.footHeight * 4 / 5 ) {
          this.refreshFootText = Constant.REFRESH_FREE_TO_REFRESH;
          this.refreshFootIcon = $r("app.media.icon_refresh_down");
          this.setRefreshStatus(RefreshStatus.OverDrag);
        } else {
          this.refreshFootText = Constant.REFRESH_PULL_TO_LOAD_MORE;
          this.refreshFootIcon = $r("app.media.icon_refresh_up");
          this.setRefreshStatus(RefreshStatus.Drag);
        }
      }
    }
  }


  private onTouchUp(event: TouchEvent) {

    let currentY = event.touches[0].y;

    if (this.dragging) {
      if (currentY >= this.downY) {
        // 手指最终向下滑动
        if (this.offsetY >= 0 || (this.headHeight - Math.abs(this.offsetY)) > this.headHeight * 4 / 5) {
          this.logD("Touch   UP: 最终达到下拉刷新条件")
          this.refreshHeadIcon = $r("app.media.icon_refresh_loading");
          this.refreshHeadText = Constant.REFRESH_REFRESHING;
          this.setRefreshStatus(RefreshStatus.Refresh)
          this.scrollToTop();

          this.notifyRefreshStarted();
        } else {
          this.logD("Touch   UP: 最终未达到下拉刷新条件")
          this.refreshHeadIcon = $r("app.media.icon_refresh_down");
          this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;
          this.setRefreshStatus(RefreshStatus.Drag);
          this.scrollByTop();
        }
      } else {
        // 手指最终向上滑动
        if ((Math.abs(this.offsetY) - this.footHeight) > this.footHeight * 4 / 5 ) {
          this.logD("Touch   UP: 最终达到上拉加载更多刷新条件")
          this.refreshFootIcon = $r("app.media.icon_refresh_loading");
          this.refreshFootText = Constant.REFRESH_REFRESHING;
          this.setRefreshStatus(RefreshStatus.Refresh);
          this.scrollToBottom();

          this.notifyLoadMoreStarted();
        } else {
          this.logD("Touch   UP: 最终未达到上拉加载更多刷新条件")
          this.refreshFootIcon = $r("app.media.icon_refresh_up");
          this.refreshFootText = Constant.REFRESH_PULL_TO_LOAD_MORE;
          this.setRefreshStatus(RefreshStatus.Drag);
          this.scrollByBottom();
        }
      }
    } else {
      this.logD("Touch   UP: 没有触发拖动条件")
    }

    this.listScrollable = true;
    this.dragging = false
  }

  private showRefreshingStatus() {
    this.offsetY = 0;
    this.refreshHeadIcon = $r("app.media.icon_refresh_loading");
    this.refreshHeadText = Constant.REFRESH_REFRESHING;
    this.setRefreshStatus(RefreshStatus.Refresh);
  }

  private showLoadMoreStatus() {
    this.offsetY = -(this.footHeight + this.headHeight);
    this.refreshFootIcon = $r("app.media.icon_refresh_loading");
    this.refreshFootText = Constant.REFRESH_REFRESHING;
    this.setRefreshStatus(RefreshStatus.Refresh);
  }

  private updateRefreshStatus(status: RefreshStatus) {
    this.refreshStatus = status;
    this.touchEnabled = (status != RefreshStatus.Refresh && status != RefreshStatus.Done);
    this.notifyStatusChanged();
  }

  private scrollToBottom() {
    this.offsetY = -(this.headHeight + this.footHeight);
  }

  private scrollByBottom() {
    if (this.offsetY != -this.headHeight) {
      this.logD("scrollByBottom() start, offsetY: " + this.offsetY)
      let intervalId = setInterval(() => {
        if(this.offsetY >= -this.headHeight) {
          this.resetRefreshStatus();
          clearInterval(intervalId);
          this.logD("scrollByBottom() finish, offsetY: " + this.offsetY)
        } else {
          this.offsetY = ((this.offsetY + this.offsetStep) > -this.headHeight) ? (-this.headHeight) : (this.offsetY + this.offsetStep);
        }
      }, this.intervalTime);
    } else {
      this.logD("scrollByBottom(): already scrolled to bottom");
    }
  }

  private scrollToTop() {
    this.offsetY = 0;
  }

  private scrollByTop() {
    if (this.offsetY != -this.headHeight) {
      this.logD("scrollByTop() start, offsetY: " + this.offsetY);
      let intervalId = setInterval(() => {
        if(this.offsetY <= -this.headHeight) {
          this.resetRefreshStatus();
          clearInterval(intervalId);
          this.logD("scrollByTop() finish, offsetY: " + this.offsetY);
        } else {
          this.offsetY = ((this.offsetY - this.offsetStep) < -this.headHeight) ? (-this.headHeight) : (this.offsetY - this.offsetStep);
        }
      }, this.intervalTime);
    } else {
      this.logD("scrollByTop(): already scrolled to top")
    }
  }

  private resetRefreshStatus() {
    this.refreshStatus = RefreshStatus.Inactive;
    this.refreshHeadIcon = $r("app.media.icon_refresh_down");
    this.refreshFootIcon = $r("app.media.icon_refresh_up");
    this.offsetY = -this.headHeight;
    this.refreshFootText = Constant.REFRESH_PULL_TO_LOAD_MORE;
    this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;
    this.touchEnabled = true;
  }

  private finishRefresh(): void {
    this.refreshHeadText = Constant.REFRESH_SUCCESS;
    this.refreshHeadIcon = $r("app.media.icon_refresh_success");
    this.setRefreshStatus(RefreshStatus.Done);
    setTimeout(() => {
      this.scrollByTop();
    }, 1500);
  }

  private finishLoadMore(): void {
    this.refreshFootText = Constant.REFRESH_SUCCESS;
    this.refreshFootIcon = $r("app.media.icon_refresh_success");
    this.setRefreshStatus(RefreshStatus.Done);
    setTimeout(() => {
      this.scrollByBottom();
    }, 1500);
  }

  private notifyStatusChanged() {
    if (this.onStatusChanged) {
      this.onStatusChanged(this.refreshStatus);
    }
  }

  private notifyRefreshStarted() {
    if (this.onRefresh) {
      this.onRefresh();
    }
  }

  private notifyLoadMoreStarted() {
    if (this.onLoadMore) {
      this.onLoadMore();
    }
  }

  private logD(msg: string) {
    console.log(msg + ", dragging: " + this.dragging + ", listScrollable: " + this.listScrollable)
  }
}

9.8.2:完整样例

笔者实现的 RefreshLayout 很简单,使用方式也和 Refresh 的用法保持了一致,唯一需要注意的就是想要自己实现 List 的每一个 item 局部,样例代码如下所示:

import { RefreshLayout } from './refresh_layout';

@Entry @ComponentV2 struct Page_07_loadmore {

  @Local dataSet: Array<string> = [];
  @Local refreshing: boolean = false;
  @Local loadMore: boolean = false;

  @Builder itemLayout(item: object, index: number) {
    Text("item:" + item + ", index: " + index)
      .width('80%')
      .height(100)
      .margin({top: 10})
      .fontSize(16)
      .textAlign(TextAlign.Center)
      .borderRadius(10)
      .backgroundColor('#bbccaa')
      .borderRadius(10)
  }

  build() {
    Column() {
      Row({space: 10}) {
        Button("下拉刷新")
          .onClick(() => {
            this.refreshing = true;
          })
        Button("下拉刷新结束")
          .onClick(() => {
            this.refreshing = false;
          })
        Button("上拉刷新")
          .onClick(() => {
            this.loadMore = true;
          })
        Button("上拉刷新结束")
          .onClick(() => {
            this.loadMore = false;
          })
      }
      .width("100%")
      .height(50)

      Column() {
        RefreshLayout({
          refreshing: this.refreshing!!,
          loadMore: this.loadMore!!,
          dataSet: this.dataSet,
          itemLayout: (item: Object, index: number) => {
            this.itemLayout(item, index);
          },
          onRefresh: () => {
            this.refreshing = true;
            this.doRefresh();
          },
          onLoadMore: () => {
            this.loadMore = true;
            this.doLoadMore();
          },
          onStatusChanged: (status) => {
            console.log("current status: " + status);
          }
        })
      }
      .clip(true)
      .width("100%")
      .layoutWeight(1)
    }
    .width("100%")
    .width('100%')
  }

  aboutToAppear() {
    this.initDataSet((Math.random() * 100), 10);
  }

  private doRefresh() {
    setTimeout(() => {
      console.log("finish refresh")
      this.initDataSet((Math.random() * 100), 10);
      this.refreshing = false;
    }, 2500);
  }

  private doLoadMore() {
    setTimeout(() => {
      console.log("finish loadMore");
      let count = this.dataSet.length;
      let dataSet = new Array<string>();
      for (let i = 0; i < count; i++) {
        dataSet.push(this.dataSet[i]);
      }
      let rand = (Math.random() * 100);
      for (let i = 0; i < 10; i++) {
        dataSet.push((i + rand).toFixed(2));
      }
      this.dataSet = dataSet;
      this.loadMore = false;
    }, 2500);
  }

  private initDataSet(start: number, count: number) {
    let dataSet = new Array<string>();
    for (let i = 0; i < count; i++) {
      dataSet.push((i + start).toFixed(2));
    }
    this.dataSet = dataSet;
  }
}

备注

作者:灰太狼

出处:https://www.arkui.club/

链接:https://www.nutpi.net/

來源:坚果派

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。否则追究相关责任。

收藏00

登录 后评论。没有帐号? 注册 一个。