赞
踩
目前货拉拉作为首批和鸿蒙合作适配的厂商 之一,已经在内部开始适配鸿蒙版货拉拉用户端
在鸿蒙开发适配过程中发现,项目中存有列表+分组的场景,按目前已有实现方式存在如下问题:
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
头部下拉刷新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 }) } }
触发下拉刷新的方式,则是通过监听控件的 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; //... } }
当松开手指后,根据此前下拉滑动时记录的已满足下拉刷新的标记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);
}
}
由于使用 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)
}
}
动态分组是指在获取到数据之后才能去实现分组,而不是像通讯录那种可以一次获取所有列表数据和分组数据
如果是后端给的数据已经实现分组,则可以直接按照给的分组进行 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 }
底部上拉加载视图为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) } }
实现上拉加载更多逻辑,需要先获取是否当前已经滑动到当前页的最后一条数据了,获取的方法是通过.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 ; } } }
获取到上面的标记之后,则在手指松开之后,会调用获取下一页的数据,这样就完成了上拉加载更多
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); } }
@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) // 必须设置列表为滑动到边缘无效果 }
通过分别构造滑动时假 head 头和未滑动时的 head 头,第一个 head 头在滑动后,通过监听onScrollIndex首条出现的ListItem 的角标动态设置数据,并且该控件处在 UI在 List 控件之上,达到悬停的效果
第二个 head 头与第一个 head 头互斥出现,滑动后即消失,在视觉上就像是通讯录分组一样的效果
八、类接口说明
1.RefreshLayout:下拉刷新的UI控件,可定制
2.itemHead:分组 head 头
3.LoadMoreLayout:上拉加载更多 UI 空间,可定制
4.NoMoreLayout:没有更多 UI 空间,可定制
5.PullRefreshModel:用于控制下拉刷新和上拉加载状态记录的 model 类
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。