当前位置:   article > 正文

【鸿蒙实战开发教程】支持刷新加载的HarmonyOS动态分组列表组件_鸿蒙上拉加载

鸿蒙上拉加载

一、背景

目前货拉拉作为首批和鸿蒙合作适配的厂商 之一,已经在内部开始适配鸿蒙版货拉拉用户端
鸿蒙开发适配过程中发现,项目中存有列表+分组的场景,按目前已有实现方式存在如下问题:

1.官方文档上推荐实现的分组列表是使用ListItemGroup的方式来实现分组

2.ListItemGroup适用于静态分组,例如已经获取了全部数据之后通讯录或者城市列表分组显示

不太适用于

1.需要动态加载更多数据之后给数据动态分组
2.需要实时监听item滑动位置的上拉加载更多的场景

因为ListItemGroup被当做一个整体的item,难以实时监听到内部item的滑动位置,所以难以判断需要上拉加载更多

本文的PullToRefresh组件在开源的下拉刷新组件的基础上同时实现下拉刷新、上拉加载更多、列表动态分组功能

二、简介

PullToRefreshFor是鸿蒙下可同时实现动态分组列表进行下拉刷新、上拉加载的组件
在以下版本验证通过:

●DevEco Studio: 4.1 Canary(4.1.3.500), SDK: API11 (4.1.0)

理论上也支持API 9、10的版本

三、功能特性

●特性1:支持下拉刷新和上拉加载更多数据
●特性2:同时支持动态分组列表

和这个gitee.com/openharmony…

1.监听手势事件的方式不同:PullToRefresh 使用parallelGesture方法获取触摸手势事件,本组件使用onTouch方法获取手势
2.灵活度不同:PullToRefresh把整个组件进行一个大的封装,由外部传入 List 组件和数据请求函数即可,优点是使用上手简单,缺点是不太容易定制。本组件则是把下拉刷新、上拉加载、Head 作为单独的组件供外部使用,优点是可自由定制如实现本次分组列表,缺点是需要多处声明

四、安装指南

ohpm install @huolala/pull-refresh
  • 1

五、代码示例

1、头部刷新部分及头部刷新逻辑

头部下拉刷新UI视图组件为CustomRefreshLoadLayout,当需要下拉刷新时,传入PullRefreshModel里的refreshLayoutConfig,然后添加此组件即可预设刷新 UI
通过@state 注解的 PullRefreshModel 类,当满足相应条件时,自动更新是否可见、刷新时的图片资源、刷新时的文案,控件高度
如当外部更改为可见时则使用预设控件高度显示,否则高度置为 0,则隐藏了刷新控件

 // 下拉刷新
CustomRefreshLoadLayout({ config: this.dataModel.refreshLayoutConfig })

@Observed
export  class PullRefreshModel {
  //...
  refreshLayoutConfig: CustomRefreshLoadLayoutConfig = new CustomRefreshLoadLayoutConfig(false)
  //...
}

@Component
export  default struct CustomLayout {
  @ObjectLink customRefreshLoadClass: CustomRefreshLoadLayoutClass;

  build() {
    Row() {
      // UI 视图,跟随状态是动态获取
      // ....省略具体UI
    }
    .clip(true)
    .width(Const.FULL_WIDTH)
    .justifyContent(FlexAlign.Center)
    // 这里通过获取刷新组件是否可见的值,来动态控制的高度是否为 0
    .height(this.customRefreshLoadClass.isVisible == true ? this.customRefreshLoadClass.heightValue : 0)
    .animation({
      duration: 300
    })
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

触发下拉刷新的方式,则是通过监听控件的 onTouch方法,传入 TouchEvent 触摸数据到组件内部,通过判断下滑偏移量来更新下拉刷新组件的PullRefreshModel类的属性值,最后通过数据更新 UI 到上面的CustomRefreshLoadLayout中

export  function touchMovePullRefresh(dataModel: PullRefreshModel, event: TouchEvent) {
  if (dataModel.startIndex === 0) {
    // 表示已经可以操作下拉刷新
    dataModel.isPullRefreshOperation = true;
    let height = vp2px(dataModel.pullDownRefreshHeight);
    dataModel.offsetY = event.touches[0].y - dataModel.downY;
    // 偏移达到刷新的值.
    if (dataModel.offsetY >= height) {
      pullRefreshState(dataModel, RefreshState.Release);
      dataModel.offsetY = height + dataModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;
    } else {
      // 偏移没达到刷新的值.继续显示“下拉刷新”
      pullRefreshState(dataModel, RefreshState.DropDown);
    }
    if (dataModel.offsetY < 0) {
      dataModel.offsetY = 0;
      dataModel.isPullRefreshOperation = false;
    }
  }
}

export  function pullRefreshState(dataModel: PullRefreshModel, state: number) {
  switch (state) {
    case RefreshState.DropDown:
      dataModel.refreshLayoutConfig.textValue = $r('app.string.pull_down_refresh_text');
      dataModel.refreshLayoutConfig.imageSrc = $r('app.media.client_ic_pull_down_refresh');
      dataModel.isCanRefresh = false;
      dataModel.isRefreshing = false;
      dataModel.refreshLayoutConfig.isVisible = true;
  break;
    case RefreshState.Release:
      dataModel.refreshLayoutConfig.textValue = $r('app.string.release_refresh_text');
      dataModel.refreshLayoutConfig.imageSrc = $r('app.media.client_ic_pull_up_refresh');
      dataModel.isCanRefresh = true;
      dataModel.isRefreshing = false;
      break;
    //...
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

当松开手指后,根据此前下拉滑动时记录的已满足下拉刷新的标记isCanRefresh,满足则回调请求数据,即完成一次下拉刷新

export  function touchUpPullRefresh(dataModel: PullRefreshModel, getDataCallBack: (isLoadMore: boolean) => void) {
  if (dataModel.isCanRefresh === true) {
    // 满足可以刷新请求数据
    dataModel.offsetY = vp2px(dataModel.pullDownRefreshHeight);
    pullRefreshState(dataModel, RefreshState.Refreshing);
    // 页码置为 1
    dataModel.currentPage = 1;
    getDataCallBack(false)
  } else {
    closeRefresh(dataModel, false);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2、占位head及列表head部分及交互逻辑

由于使用 ListItemGroup 会无法监听到 ListItemGroup 内部的 Item,但业务场景仍然需要分组的 UI,所以这里使用单独的占位 head 去作为分组标题的来显示
占位 head 总共有两处,一处是在 List 列表布局外面,一个是 List列表首条 Item 里
这两条 head 的用处分别是,第一条 head 用于在滑动的时候,始终悬浮在最顶部,并且通过onScrollIndex方法获取到当前首条 Item,数据来动态更新占位 head 的数据

Row() {
    // 1. 假的占位 head 头
    this.itemHead()
  }
  .visibility(this.showFakeHead? Visibility.Visible : Visibility.None)

 List({space:20, scroller: this.scroller }) {
    ListItem() {
      Row() {
          // 2. 列表的head头
        this.itemHead()
      }.visibility(!this.showFakeHead? Visibility.Visible : Visibility.None)
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3、列表动态分组实现逻辑

动态分组是指在获取到数据之后才能去实现分组,而不是像通讯录那种可以一次获取所有列表数据和分组数据

如果是后端给的数据已经实现分组,则可以直接按照给的分组进行 UI 渲染,然后直接进行下一页获取即可。但如果是后端给的数据里没有包含任何分组数据,则需要由我们来进行动态分组和更新数据来渲染 UI

具体做法是构建一个用来展示的 model 类的数据集合,在拿到原始数据的时候,判断每一条 head 的数据和之前记录的 head 数据是否相符,如果不符,则手动插入一条 head 数据,这条数据仅用来显示分组的标题,如果相同则继续添加原来的数据进去新的集合,只是这是一条普通的 Item 数据,最后取新的集合展示数据

let currentHead: string = ""
private  getList(data: ListData): ListDisplayBean[] {
  let  listDisplay: ListDisplayBean[] = []

  if (data.list == null || data.list == undefined) {
    return orderList
  }
  for (let i = 0; i < data.list.length; i++) {
    let item = data.list[i]
    if (this.currentHead != item.head) {
      // 
      bean.isMonth = true
orderList.push(bean)
    }

    let bean = new  ListDisplayBean()
    bean.item = item
    listDisplay.push(bean)
    this.currentHead = item.head
  }
  return orderList
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

4、底部加载更多部分及加载更多逻辑

底部上拉加载视图为CustomRefreshLoadLayout,和下拉刷新一样,复用同样的一个UI组件,只是传入的数据不一样
与下拉刷新不同的是,必须是有下一页数据时才会显示这个组件,是否有下一页数据,则在每次请求完数据的时候根据条数确定,否则显示没有更多数据的组件

/**
 * 上拉加载更多组件
 */
@Component
export struct LoadMoreLayout {
  @ObjectLink loadMoreLayoutClass: CustomRefreshLoadLayoutClass;

  build() {
    Column() {
      CustomRefreshLoadLayout({
        customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible,
          this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, this.loadMoreLayoutClass.heightValue)
      })
    }
  }
}

/**
 * 没有更多数据组件.
 */
@Component
export struct NoMoreLayout {
  build() {
    Row() {
      Text('没有更多数据了')
        .margin({ left: Const.NoMoreLayoutConstant_NORMAL_PADDING })
        .fontSize(Const.NoMoreLayoutConstant_TITLE_FONT)
        .textAlign(TextAlign.Center)
    }
    .width(Const.FULL_WIDTH)
    .justifyContent(FlexAlign.Center)
    .height(Const.CUSTOM_LAYOUT_HEIGHT)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

实现上拉加载更多逻辑,需要先获取是否当前已经滑动到当前页的最后一条数据了,获取的方法是通过.onScrollIndex里当前滚动数据的角标,如果最后一条数据角标大于当前该页全部的数据的大小,则表示已经滑到该页最后一条数据。然后继续判断是否已经达到上拉触发的滑动阈值,达到就修改标记为已触发上拉加载更多

export  function  touchMoveLoadMore ( dataModel: PullRefreshModel, event: TouchEvent ) {  if (dataModel. endIndex >= dataModel. dataSize - 1 ) { dataModel. offsetY = event. touches [ 0 ]. y - dataModel. downY ;  if ( Math . abs (dataModel. offsetY ) > vp2px (dataModel. pullUpLoadHeight ) / 2 ) { dataModel. isCanLoadMore = true ; dataModel. loadMoreLayoutConfig . isVisible = true ; dataModel. offsetY = - vp2px (dataModel. pullUpLoadHeight ) + dataModel. offsetY * Const . Y_OFF_SET_COEFFICIENT ; } } } 
  • 1

获取到上面的标记之后,则在手指松开之后,会调用获取下一页的数据,这样就完成了上拉加载更多

export  function  touchUpLoadMore ( dataModel: PullRefreshModel, getDataCallBack: (isLoadMore: boolean ) => void ) {  let  self : PullRefreshModel = dataModel;  animateTo ({  duration : Const . ANIMATION_DURATION , }, () => { self. offsetY = 0 ; })    // isCanLoadMore 为 true 表示当前已经到第一页最后一条数据并且手势上滑到了阈值    // hasMore 为 true 表示数据还有下一页,默认是 true   if ((self. isCanLoadMore === true ) && (self. hasMore === true )) { self. isLoading = true ;  getDataCallBack ( true ) } else {  closeLoadMore (self); } } 
  • 1

5、整个列表的逻辑部分

@State data: GroupData[] = [];
@State headTitle: GroupData = new GroupData()
@State showFakeHead: boolean = true
// 需绑定列表或宫格组件
private scroller: Scroller = new Scroller();
@State private dataModel: PullRefreshModel = new PullRefreshModel()
private itemDataGroupNew: GroupData[] = [....]// 假数据省略

@Builder
private getListView() {
    // 列表首条 Item
  CustomRefreshLoadLayout({ config: this.dataModel.refreshLayoutConfig })
    

   // 1. 假的占位 head 头
  Row() {
    this.itemHead()
  }
  .visibility(this.showFakeHead? Visibility.Visible : Visibility.None)

  List({space:20, scroller: this.scroller }) {
    ListItem() {
      Row() {
          // 2. 列表的head头
        this.itemHead()
      }.visibility(!this.showFakeHead? Visibility.Visible : Visibility.None)
    }

    ForEach(this.data, (item: GroupData) => {
      ListItem() {
        Column() {
          Row() {
            // 3. 列表中不悬浮的 head
            Text(item.head)
              .fontSize(20)
              .height(50)
              .backgroundColor('#FF667075')
              .width('100%')
          }.visibility(item.isHead ? Visibility.Visible : Visibility.None)

          Text(item.content)
            .width('100%')
            .height(150)
            .fontSize(20)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FF6600')
            .visibility(!item.isHead ? Visibility.Visible : Visibility.None)
        }
      }
    })
    // 列表末条 Item
    ListItem() {
      if (this.dataModel.hasMore) {
        CustomRefreshLoadLayout({ config: this.dataModel.loadMoreLayoutConfig })
      } else {
        NoMoreLayout()
      }
    }
  }
  .onTouch((event: TouchEvent | undefined) => {
    if (event) {
      if (this.dataModel.pageState === PageState.Success) {
        listTouchEvent(this.dataModel, event, (isLoadMore: boolean) => {
          this.getData(isLoadMore)
        });
      }
    }
  })
  .onScrollIndex((start: number, end: number) => {
    console.log(`headfloat start:${start}`)
    if (this.data.length > start) {
      let startValue = this.data[start]
      // 4. 赋值 head 数据
      this.headTitle = startValue
    }
    let yOffset: number = this.scroller.currentOffset().yOffset
    if (yOffset >= -0.01) {
        // 5. 控制 head 头展示
      this.showFakeHead = true
} else {
      this.showFakeHead = false
}
    this.dataModel.startIndex = start;
    this.dataModel.endIndex = end;
  })
  .backgroundColor('#eeeeee')
  .edgeEffect(EdgeEffect.None) // 必须设置列表为滑动到边缘无效果
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

七、原理说明

通过分别构造滑动时假 head 头和未滑动时的 head 头,第一个 head 头在滑动后,通过监听onScrollIndex首条出现的ListItem 的角标动态设置数据,并且该控件处在 UI在 List 控件之上,达到悬停的效果
第二个 head 头与第一个 head 头互斥出现,滑动后即消失,在视觉上就像是通讯录分组一样的效果
八、类接口说明

1.RefreshLayout:下拉刷新的UI控件,可定制
2.itemHead:分组 head 头
3.LoadMoreLayout:上拉加载更多 UI 空间,可定制
4.NoMoreLayout:没有更多 UI 空间,可定制
5.PullRefreshModel:用于控制下拉刷新和上拉加载状态记录的 model 类
在这里插入图片描述

在这里插入图片描述

总结

总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。随着鸿蒙的不断发展以及国家的大力支持,未来鸿蒙职位肯定会迎来一个大的爆发,只有积极应对变化,不断学习和提升自己,我们才能在这个变革的时代中立于不败之地。

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/1008953
推荐阅读
相关标签
  

闽ICP备14008679号