2.8、HarmonyOS Next 下拉刷新与上拉加载

50 阅读4分钟

页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的触摸事件,在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。

实现思路

以下拉刷新为例,其实现主要分成三步:

  • 监听手指按下事件,记录其初始位置的值。
  • 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。
  • 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。

实现效果

在这里插入图片描述

代码调用

import PullToRefreshView, { UpDownRefreshHandler } from '../../../widget/PullToRefreshView'

@Entry
@Component
struct PullToRefreshPage {

  @State listItems:Array<String> = []

  /**
   * 刷新控制器
   */
  upDownRefreshHandler = new UpDownRefreshHandler()

  aboutToAppear() {
    sleep(100).then(()=>{
      this.upDownRefreshHandler.autoRefresh()
    })
  }

  /**
   * 单行选项
   * @param item
   */
  @Builder itemView(item) {
      Text(`${item}`)
        .padding({left: 15, right: 15, top:10, bottom: 10})
  }

  build() {
    Navigation() {
      PullToRefreshView({
        dataList: this.listItems,// 数据集
        itemView: (item)=> this.itemView(item), // 自定义单行选项
        handler: this.upDownRefreshHandler, // 刷新控制器
        onRefresh: ()=> { // 下拉刷新
          this.refreshData()
        },
        onLoadMore: ()=> { // 上拉加载更多
          this.loadMoreData()
        }
      })
    }
    .title('下拉刷新列表')
    .titleMode(NavigationTitleMode.Mini)
  }

  /**
   * 请求刷新
   */
  private async refreshData() {
    await sleep(2000) // 模拟耗时,两秒等待
    this.listItems.length = 0
    for (var i =0;i< 20;i++) {
      this.listItems.push(`Item${i+1}`)
    }
    this.upDownRefreshHandler.closeRefresh(true)
    this.upDownRefreshHandler.closeLoadMore(true)
  }

  /**
   * 请求加载更多
   */
  private async loadMoreData() {
    await sleep(2000) // 模拟耗时,两秒等待
    for (var i =0;i< 10;i++) {
      this.listItems.push(`Item${this.listItems.length + i+1}`)
    }
    this.upDownRefreshHandler.closeLoadMore(false)
  }
}

const sleep = (duration:number)=> {
    return new Promise((resolve)=> {
       setTimeout(resolve, duration)
    })
}

自定义下拉刷新组件

/**
 * 实现思路
 * 监听手指按下事件,记录其初始位置的值。
 * 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。
 * 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。
 */
const TopHeight = 200;

@Component
export default struct PullToRefreshView {
  @State dataList: Array<any> = [];
  onRefresh : ()=> void
  onLoadMore : ()=> void
  handler:UpDownRefreshHandler
  @BuilderParam itemView: (item) => void;
  // 列表y坐标偏移量
  @State offsetY: number = 0
  // 按下的y坐标
  private downY = 0
  // 上一次移动的y坐标
  private lastMoveY = 0
  // 当前列表首部的索引
  private startIndex = 0
  // 当前列表尾部的索引
  private endIndex = 0

  // 下拉刷新的布局高度
  private pullRefreshHeight = 70
  // 下拉刷新文字:下拉刷新、松开刷新、正在刷新、刷新成功
  @State pullRefreshText: Resource= $r("app.string.pull_down_refresh_text")
  // 下拉刷新图标:与文字对应
  @State pullRefreshImage: Resource = $r("app.media.ic_pull_down_refresh")
  // 是否可以刷新:未达到刷新条件,收缩回去
  private isCanRefresh = false
  // 是否正在刷新:刷新中不进入触摸逻辑
  private isRefreshing: boolean = false
  // 是否已经进入了下拉刷新操作
  private isPullRefreshOperation = false

  // 上拉加载的布局默认高度
  private loadMoreHeight = 70
  // 上拉加载的布局是否显示
  @State isVisibleLoadMore: boolean = false
  // 是否可以加载更多
  private isCanLoadMore = true
  // 是否加载中:加载中不进入触摸逻辑
  private isLoading: boolean = false
  // 下拉刷新文字:下拉刷新、松开刷新、正在刷新、刷新成功
  @State pullLoadMoreText: Resource= $r("app.string.pull_up_load_text")
  // 下拉刷新图标:与文字对应
  @State pullLoadMoreImage: Resource = $r("app.media.ic_pull_up_load")

  aboutToAppear() {
    this.handler.closeRefresh = (success:boolean)=> {
      this.closeRefresh(success)
    }
    this.handler.closeLoadMore = (hasMore:boolean)=> {
      this.closeLoadMore(hasMore)
    }
    this.handler.autoRefresh = ()=> {
      this.autoRefresh()
    }
  }

  build() {
    Column() {
      // 下拉刷新布局
      this.CustomPullRefreshLayout()
      // 列表布局
      List() {
        ForEach(this.dataList, item => {
          ListItem() {
            this.itemView(item)
          }
        }, item => item.toString())

        // 加载更多布局
        ListItem(){
          this.CustomLoadMoreLayout()
        }
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.White) // 背景
      .divider({ color: '#e2e2e2', strokeWidth: 1 }) // 分割线
      .edgeEffect(EdgeEffect.None) // 去掉回弹效果
      .offset({ x: 0, y: `${this.offsetY - TopHeight}px` }) // touch事件计算的偏移量单位是px,记得加上单位
      .onScrollIndex((start, end) => { // 监听当前列表首位索引
        console.info(`${start}=start============end=${end}`)
        this.startIndex = start
        this.endIndex = end
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f4f4f4')
    .onTouch((event) => this.listTouchEvent(event))// 父容器设置touch事件,当列表无数据也可以下拉刷新
    .onAppear(() => {
    })
  }

  // 自定义下拉刷新布局
  @Builder CustomPullRefreshLayout(){
      Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
        Image(this.pullRefreshImage)
          .width(18)
          .height(18)

        Text(this.pullRefreshText)
          .margin({ left: 7, bottom: 1 })
          .fontSize(17)
      }
      .width('100%')
      .height(this.pullRefreshHeight)
      .opacity(this.offsetY / this.pullRefreshHeight)
  }

  // 自定义加载更多布局
  @Builder CustomLoadMoreLayout(){
    Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
      if (this.pullLoadMoreImage) {
        Image(this.pullLoadMoreImage)
          .width(18)
          .height(18)
      }

      Text(this.pullLoadMoreText)
        .margin({ left: 7, bottom: 1 })
        .fontSize(17)
    }
    .width('100%')
    .height(this.loadMoreHeight)
    .backgroundColor('#f4f4f4')
    .visibility(this.isVisibleLoadMore ? Visibility.Visible : Visibility.None)
  }

  // 触摸事件
  listTouchEvent(event: TouchEvent){
    switch (event.type) {
      case TouchType.Down: // 手指按下
      // 记录按下的y坐标
        this.downY = event.touches[0].y
        this.lastMoveY = event.touches[0].y
        break
      case TouchType.Move: // 手指移动
      // 下拉刷新中 或 加载更多中,不进入处理逻辑
        if(this.isRefreshing || this.isLoading){
          console.info('========Move刷新中,返回=========')
          return
        }
      // 判断手势
        let isDownPull = event.touches[0].y - this.lastMoveY > 0
      // 下拉手势 或 已经进入了下拉刷新操作
        if ((isDownPull || this.isPullRefreshOperation)) {
          console.log(`滑动操作 下拉刷新 isDownPull:${isDownPull} this.isPullRefreshOperation:${this.isPullRefreshOperation} this.isCanLoadMore:${this.isCanLoadMore}`);
          this.touchMovePullRefresh(event)
        } else {
          console.log(`滑动操作 上拉加载 isDownPull:${isDownPull} this.isPullRefreshOperation:${this.isPullRefreshOperation} this.isCanLoadMore:${this.isCanLoadMore}`);
          this.touchMoveLoadMore(event)
        }
        this.lastMoveY = event.touches[0].y
        break
      case TouchType.Up: // 手指抬起
      case TouchType.Cancel: // 触摸意外中断:来电界面
      // 刷新中 或 加载更多中,不进入处理逻辑
        if(this.isRefreshing || this.isLoading){
          console.info('========Up刷新中,返回=========')
          return
        }
        if (this.isPullRefreshOperation) {
          this.touchUpPullRefresh()
        } else {
          this.touchUpLoadMore()
        }
        break
    }
  }

  //============================================下拉刷新==================================================
  autoRefresh() {
    animateTo({ duration: 300 }, () => {
      this.isPullRefreshOperation = true
      // 下拉刷新布局高度
      var height = vp2px(this.pullRefreshHeight)
      // 滑动的偏移量
      this.offsetY = vp2px(this.pullRefreshHeight + 10) - this.downY

      // 偏移量大于下拉刷新布局高度,达到刷新条件
      if (this.offsetY >= height) {
        // 状态1:松开刷新
        this.pullRefreshState(PullToRefreshState.ReleaseToRefresh)
        // 偏移量的值缓慢增加
        this.offsetY = height + this.offsetY * 0.15
      } else {
        // 状态0:下拉刷新
        this.pullRefreshState(PullToRefreshState.PullDownToRefresh)
      }

      if (this.offsetY < 0) {
        this.offsetY = 0
        this.isPullRefreshOperation = false
      }
      this.touchUpPullRefresh()
    })
  }

  // 手指移动,处理下拉刷新
  touchMovePullRefresh(event:TouchEvent) {
    // 当首部索引位于0
    if (this.startIndex == 0) {
      this.isPullRefreshOperation = true
      // 下拉刷新布局高度
      var height = vp2px(this.pullRefreshHeight)
      // 滑动的偏移量
      this.offsetY = event.touches[0].y - this.downY

      // 偏移量大于下拉刷新布局高度,达到刷新条件
      if (this.offsetY >= height) {
        // 状态1:松开刷新
        this.pullRefreshState(PullToRefreshState.ReleaseToRefresh)
        // 偏移量的值缓慢增加
        this.offsetY = height + this.offsetY * 0.15
      } else {
        // 状态0:下拉刷新
        this.pullRefreshState(PullToRefreshState.PullDownToRefresh)
      }
      if (this.offsetY < 0) {
        this.offsetY = 0
        this.isPullRefreshOperation = false
      }
    }
  }

  // 手指抬起,处理下拉刷新
  touchUpPullRefresh(){
    // 是否可以刷新
    if (this.isCanRefresh) {
      console.info('======执行下拉刷新========')
      // 偏移量为下拉刷新布局高度
      this.offsetY = vp2px(this.pullRefreshHeight)
      // 状态2:正在刷新
      this.pullRefreshState(PullToRefreshState.Refreshing)

      this.onRefresh()
    } else {
      console.info('======关闭下拉刷新!未达到条件========')
      // 关闭刷新
      this.closeRefresh(true)
    }
  }

  // 0下拉刷新、1松开刷新、2正在刷新、3刷新成功
  /**
   * 下拉刷新状态
   * @param state 刷新状态
   */
  pullRefreshState(state:PullToRefreshState){
    switch (state) {
      case PullToRefreshState.PullDownToRefresh:
      // 初始状态
        this.pullRefreshText = $r("app.string.pull_down_refresh_text")
        this.pullRefreshImage = $r("app.media.ic_pull_down_refresh")
        this.isCanRefresh = false
        this.isRefreshing = false
        break;
      case PullToRefreshState.ReleaseToRefresh:
        this.pullRefreshText = $r("app.string.release_refresh_text")
        this.pullRefreshImage = $r("app.media.ic_pull_up_refresh")
        this.isCanRefresh = true
        this.isRefreshing = false
        break;
      case PullToRefreshState.Refreshing:
        this.offsetY = vp2px(this.pullRefreshHeight)
        this.pullRefreshText = $r("app.string.refreshing_text")
        this.pullRefreshImage = $r("app.media.ic_pull_up_load")
        this.isCanRefresh = true
        this.isRefreshing = true
        break;
      case PullToRefreshState.RefreshSuccess:
        this.pullRefreshText = $r("app.string.refresh_success_text")
        this.pullRefreshImage = $r("app.media.ic_succeed_refresh")
        this.isCanRefresh = true
        this.isRefreshing = true
        break;
      case PullToRefreshState.RefreshFailed:
        this.pullRefreshText = $r("app.string.refresh_fail_text")
        this.pullRefreshImage = $r("app.media.ic_fail_refresh")
        this.isCanRefresh = true
        this.isRefreshing = true
        break;
    }
  }

  // 关闭刷新
  closeRefresh(success:boolean) {
    // 如果允许刷新,延迟进入,为了显示刷新中
    setTimeout(() => {
      var delay = 50
      if (this.isCanRefresh) {
        // 状态3:刷新成功
        this.pullRefreshState(success ? PullToRefreshState.RefreshSuccess : PullToRefreshState.RefreshFailed)
        // 为了显示刷新成功,延迟执行收缩动画
        delay = 500
      }
      animateTo({
        duration: 150, // 动画时长
        delay: delay, // 延迟时长
        onFinish: () => {
          // 状态0:下拉刷新
          this.pullRefreshState(PullToRefreshState.PullDownToRefresh)
          this.isPullRefreshOperation = false
        }
      }, () => {
        this.offsetY = 0
      })
    }, this.isCanRefresh ? 500 : 0)
  }

  //============================================加载更多==================================================
  // 手指移动,处理加载更多
  touchMoveLoadMore(event:TouchEvent) {
    // 因为加载更多是在列表后面新增一个item,当一屏能够展示全部列表,endIndex 为 length+1
    if (this.endIndex !== this.dataList.length || this.endIndex !== this.dataList.length - 1) {
      // 滑动的偏移量
      this.offsetY = event.touches[0].y - this.downY
      if (Math.abs(this.offsetY) > vp2px(this.loadMoreHeight)/2) {
        // 显示加载更多布局
        this.isVisibleLoadMore = true
        // 偏移量缓慢增加
        this.offsetY = - vp2px(this.loadMoreHeight) + this.offsetY * 0.1
      }
    }
  }

  // 手指抬起,处理加载更多
  touchUpLoadMore() {
    animateTo({
      duration: 200, // 动画时长
    }, () => {
      // 偏移量设置为0
      this.offsetY = 0
    })
    if (this.isCanLoadMore) {
      console.info('======执行加载更多========')
      // 加载中...
      this.isLoading = true
      this.onLoadMore()
    } else {
      console.info('======关闭加载更多!未达到条件========')
      this.closeLoadMore(false)
    }
  }

  // 关闭加载更多
  closeLoadMore(hasMore:boolean) {
    this.isLoading = false
    this.isCanLoadMore = hasMore
    if (hasMore) {
      this.isVisibleLoadMore = false
      this.pullLoadMoreImage = $r("app.media.ic_pull_up_load")
      this.pullLoadMoreText = $r("app.string.pull_up_load_text")
    } else {
      this.pullLoadMoreImage = null
      this.pullLoadMoreText = $r("app.string.pull_up_no_more_text")
      this.isVisibleLoadMore = true
    }
  }
}


export class UpDownRefreshHandler {
      autoRefresh:()=> void
      closeRefresh:(success:boolean)=> void
      closeLoadMore:(hasMore:boolean)=> void
}

enum PullToRefreshState {
    PullDownToRefresh,
    ReleaseToRefresh,
    Refreshing,
    RefreshSuccess,
    RefreshFailed,
}

上一篇 2.7、创建列表(List) 下一篇 2.9、创建网格(Grid/GridItem)