一、写在前面
鸿蒙开发,如火如荼,随着第一版本的上线,节奏暂时可以放慢一点,趁现在回过头对之前的东西优化一下,之前的列表刷新控件不太符合设计要求,因此在此做下优化,重写了一个。
二、目标
这次的开发本质是对之前的功能在鸿蒙上做一下实现,贴个图展示一下效果:
功能很简单,就是下拉刷新,上拉加载更多,同时加载完数据后显示没有更多的布局
三、开始吧
1、刷新
刷新使用的是官方ReFresh控件,没什么好说的,我们只需要监听onRefreshing回调即可:
2、加载更多
滑动初始状态
我们都知道,加载更多操作是由手势拖动触发的,所以这个肯定是需要今天空间的onTouch事件了,然后我们考虑下页面结构,底部的加载更多布局和上面的列表可以是Column, Y轴上下结构,也可以用Stack,Z轴上下覆盖,然后给底部的加载更多布局一个初始偏移量,移出屏幕。
kotlin
复制代码
// "加载更多"布局的偏移量 外部无需关心
@State private dragY: number = this.loadMoreViewHeight
Row() {
this.LoadFooter()
}.width("100%")
.height(this.loadMoreViewHeight)
.translate({ y: this.dragY })
拖动状态
TouchType.Down:
ini
复制代码
// 拖动过程中,y轴的起始拖动位置, 用于计算上面的dragY
private yStart: number = 0; // list触摸事件起始纵坐标
this.yStart = event.touches[0].y;
此处制作一件事,就是标记手指按下的y轴起始坐标。
TouchType.Move:
这是整个控件的核心位置,处理拖动的主要逻辑,先贴下这部分的完整代码:
kotlin
复制代码
// 标记当前位置的坐标
const yEnd = event.touches[0].y; // 手指离开屏幕的纵坐标
// 已经滑到底部了
if (this.scroller.isAtEnd() && !this.isLoading) {
// 展示底部局部
this.isShowLoadingWhenTouch = true
// 计算加载更多布局的偏移量;*0.6是用来调手感的,
const changeY = (yEnd - this.yStart) * 0.6 + this.loadMoreViewHeight
// 偏移量边界处理
this.dragY = changeY < -this.loadMoreViewHeight ? -this.loadMoreViewHeight : changeY
console.debug(`this.dragY = ${this.dragY}`)
// 判断上滑,且list跟随手势滑动
if (yEnd <= this.yStart) {
// 标记手势Up时,是否要回调加载更多
if (changeY < this.loadMoreViewHeight) {
this.isLoadMoreWhenUp = true
} else {
this.isLoadMoreWhenUp = false
}
} else {
// 如果往下滑,且超过了起始位置,则置回起始位置,同时更新手势的起始位置
if (this.dragY >= this.loadMoreViewHeight) {
this.dragY = this.loadMoreViewHeight
this.isLoadMoreWhenUp = false
this.yStart = yEnd
} else {
// 同上面yEnd <= this.yStart的逻辑
if (changeY < this.loadMoreViewHeight) {
this.isLoadMoreWhenUp = true
} else {
this.isLoadMoreWhenUp = false
}
}
}
} else {
// 没到底部 就置回起始状态
this.isShowLoadingWhenTouch = false
this.yStart = yEnd
if (!this.scroller.isAtEnd()) {
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
}, () => {
this.dragY = this.loadMoreViewHeight
})
}
}
简单说下逻辑,先判断是不是滑到最底下,这个可以使用List的scroller.isAtEnd() 来获取,this.isLoading是当前加载中的标记,防止多次加载:
kotlin
复制代码
if (this.scroller.isAtEnd() && !this.isLoading)
以此为界限,实时计算当前的偏移量,同时做边界处理:
kotlin
复制代码
// 展示底部局部
this.isShowLoadingWhenTouch = true
// 计算加载更多布局的偏移量;*0.6是用来调手感的,
const changeY = (yEnd - this.yStart) * 0.6 + this.loadMoreViewHeight
// 偏移量边界处理
this.dragY = changeY < -this.loadMoreViewHeight ? -this.loadMoreViewHeight : changeY
根据yEnd <= this.yStart来判断是上滑还是下滑,上滑时这个偏移量小于起始位置(位置在起始位置的上面,this.loadMoreViewHeight是初始值) ,则做个标记,用于手势抬起时做加载回调;
下滑时,如果到达初始位置,则更新起始位置,如果没有,则也要判断是否达到可触发回调的逻辑,因为拖动过程中,手指不离开屏幕,可以来回滑:
kotlin
复制代码
if (yEnd <= this.yStart) {
// 标记手势Up时,是否要回调加载更多
if (changeY < this.loadMoreViewHeight) {
this.isLoadMoreWhenUp = true
} else {
this.isLoadMoreWhenUp = false
}
} else {
// 如果往下滑,且超过了起始位置,则置回起始位置,同时更新手势的起始位置
if (this.dragY >= this.loadMoreViewHeight) {
this.dragY = this.loadMoreViewHeight
this.isLoadMoreWhenUp = false
this.yStart = yEnd
} else {
// 同上面yEnd <= this.yStart的逻辑
if (changeY < this.loadMoreViewHeight) {
this.isLoadMoreWhenUp = true
} else {
this.isLoadMoreWhenUp = false
}
}
}
TouchType.Up:
完整代码:
kotlin
复制代码
// 手指up时,如果需要回调事件就回调,同时加载中布局全部展示,否则置回起始状态
if (this.isLoadMoreWhenUp) {
if (!this.isRefreshing && !this.isLoading && !this.isNoMoreData) {
this.loadMore()
this.isLoading = true
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
}, () => {
this.dragY = 0
})
} else {
this.dragY = this.loadMoreViewHeight
this.isShowLoadingWhenTouch = false
}
} else {
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
onFinish: () => {
this.isShowLoadingWhenTouch = false
}
}, () => {
this.dragY = this.loadMoreViewHeight
})
}
这部分就是根据之前标记的isLoadMoreWhenUp字段,同时当前没有在加载中状态来触发回调(同时展示完整的加载中布局),其他情况都是动画变成起始状态。
3、惯性滚动
这里有一种其他状态,就是惯性滚动,快速滑动屏幕的过程中,由于惯性,列表到底后,会继续带有回弹效果地滚动,这种状态我们也需要响应逻辑,不然每次到达底部后,加载下一页都需要手指再拖动一下,体验不好,贴下代码:
kotlin
复制代码
.onScroll((scrollOffset: number, scrollState: ScrollState) => {
this.onScroll(scrollOffset, scrollState)
// 惯性滚动时 达到底部后 回弹过程中
if (scrollState === ScrollState.Fling) {
// 累加持续滚动的距离
this.yStartFling += scrollOffset
// 已经滑到底部了 且不是在加载中
if (this.scroller.isAtEnd() && !this.isLoading) {
// 更改标记 让“加载中”布局展示
this.isShowLoadingWhenTouch = true
// 计算当前“加载中”布局的偏移量,this.dragY为当前的偏移量;之所减去yStartFling是因为偏移量往下是正方向,页面往上拖scrollState为正方向,减去yStartFling才是最终的偏移量
const changeY = this.dragY - this.yStartFling
// 赋值偏移量,边界处理,值为正负的loadMoreViewHeight
this.dragY = changeY > this.loadMoreViewHeight ? this.loadMoreViewHeight :
changeY < -this.loadMoreViewHeight ? -this.loadMoreViewHeight : changeY
// 判断上滑,且list跟随手势滑动
if (this.yStartFling > 0) {
// 如果惯性滚动的距离小于0
if (changeY < 0) {
// 判断不是在加载更多中
if (!this.isLoading && !this.isNoMoreData) {
// 执行加载更多的回调
this.loadMore()
this.isLoading = true
// 执行动画 让整个加载布局全部展示
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
}, () => {
this.dragY = 0
})
}
}
}
} else {
// 隐藏加载更多的布局
this.isShowLoadingWhenTouch = false
this.yStartFling = 0
this.dragY = this.loadMoreViewHeight
}
}
})
如代码所示,列表滚动过程中是有onScroll((scrollOffset: number, scrollState: ScrollState) 的回调的(12中环城onDidScroll() 了),第一个参数是位移值,第二个是滚动的状态(ScrollState),关于ScrollState有三种状态:
- Idle:空闲状态。使用控制器提供的方法控制滚动时触发,拖动滚动条滚动时触发
- Scroll:滚动状态。使用手指拖动List滚动时触发。
- Fling:惯性滚动状态。快速划动松手后进行惯性滚动和划动到边缘回弹时触发。
所以此处我们使用Fling状态来过滤时间即可。
这里注意下scrollOffset,再滚动过程中,往上拖动列表惯性滚动时,也就是列表往下滚时,scrollOffset是负数,所以这里const changeY = this.dragY - this.yStartFling, 需要减去yStartFling,才是真正的偏移量。
整个惯性滚动,我们只需要判断是触发加载更多的回调即可(完全展示加载更多的布局),其它都是置回初始位置。
4、控制器
控件内部的事件只有触发回调,什么时候结束加载中的状态,是由外部控制的,所以这里将时间协程控制器的形式:
typescript
复制代码
export class CppPullToRefreshController {
private cppRefreshView: CppPullToRefresh | undefined
bind(cppRefreshView: CppPullToRefresh) {
this.cppRefreshView = cppRefreshView
}
needLoadMore(value: boolean) {
this.cppRefreshView?.needLoadMore(value)
}
finishLoadMore() {
this.cppRefreshView?.finishLoadMore()
}
finishRefresh() {
this.cppRefreshView?.finishRefresh()
}
// 没有更多
noMoreData(value: boolean) {
this.cppRefreshView?.noMoreData(value)
}
}
- bind:
绑定控制前和页面的关系
- needLoadMore:
是否需要加载更多,有的页面不需要加载更多,可以直接通过这个函数设置
- finishLoadMore:
完成加载更多
- finishRefresh:
完成刷新
- noMoreData:
没有更多数据,展示后缀,没有更多数据的时候,上拉是不会触发加载更多事件的。
绑定函数是在控件的aboutToAppear函数中调用,做绑定关系,viewController对象由外部控件设置即可。
kotlin
复制代码
aboutToAppear(): void {
// 绑定Controller
this.viewController?.bind(this)
}
下面的代码是放在接口加载列表回调的地方,noMoreData是在当前列表有数据,只是当前这一页没有数据的时候,才设置为true,不然空态页有加载更多也不合理。
needLoadMore只要当前页面没有数据,就设置false。
kotlin
复制代码
if (value?.data.length === 0) {
// 当前页没有数据 且整个列表有数据 则展示没有更多块
this.viewController.noMoreData(!this.isEmpty)
} else {
this.viewController.noMoreData(false)
}
if(this.isEmpty){
this.viewController.needLoadMore(false)
}else{
this.viewController.needLoadMore(true)
}
好了,整个控件就写完了,贴下完整代码:
kotlin
复制代码
import { LoadingMoreLayout, RefreshHeaderLayout } from '@ohos/uicomponents/Index'
// 列表刷新 加载更多的控件
@Component
export struct CppPullToRefresh {
// CppPullToRefresh的控制器 控制
viewController: CppPullToRefreshController | undefined
// 外部传入布局的内容,注意这里是填充List ,所以外部传入的要用listItem 或者ListGroupItem包裹
@BuilderParam body?: () => void
// 刷新的回调
reFresh = () => {
}
// 加载更多的回调
loadMore = () => {
}
onScroll = (scrollOffset: number, scrollState: ScrollState) => {
}
//////////// 以上需要外部传入 ////////////////
// 是否需要加载更多的状态,可以直接设置,也可以通过viewController设置
// PS:直接设置只有第一次,实时的需要通过viewController设置
@State isNeedLoadMore: boolean = true
// 是否要展示"没有更多"提示的布局 默认是要的
@State isNeedNoMoreData: boolean = true
// "没有更多"展示的提示
// PS:isNeedNoMoreData为true时,isNoMoreData才会生效
@State noMoreDataText: string = '没有更多数据'
//////////// 以上也是外部传入,不是必须 ////////////////
//////////// 以下参数外部无需关注 ////////////////
// 列表滚动的scroller
private scroller: Scroller = new Scroller()
// 底部加载更多布局的高度
private loadMoreViewHeight: number = 70; // list触摸事件起始纵坐标
// 是否是刷新的状态
@State isRefreshing: boolean = false
// 是否是加载中
@State private isLoading: boolean = false
// 是否是"没有更多"的状态
// PS:isNeedNoMoreData为true时,isNoMoreData才会生效
@State private isNoMoreData: boolean = false
// "加载更多"布局的偏移量 外部无需关心
@State private dragY: number = this.loadMoreViewHeight
// 拖动过程中是否展示加载更多布局
@State private isShowLoadingWhenTouch: boolean = false
// 拖动过程中,y轴的起始拖动位置, 用于计算上面的dragY
private yStart: number = 0; // list触摸事件起始纵坐标
// 拖动过程后,手指起来时,是不是要触发加载更多的标记
private isLoadMoreWhenUp: boolean = false;
// list惯性滚动起始累加值 也是判断是否要加载更多的判断
private yStartFling: number = 0;
aboutToAppear(): void {
// 绑定Controller
this.viewController?.bind(this)
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Refresh({ refreshing: $$this.isRefreshing, builder: this.RefreshHeader() }) {
List({ scroller: this.scroller }) {
if (this.body) {
this.body()
} else {
ListItem() {
this.buildBody()
}
}
if (this.isNeedNoMoreData && this.isNoMoreData) {
ListItem() {
Text(this.noMoreDataText)
.fontColor('#FF5A5A5A')
.fontSize('12fp')
.textAlign(TextAlign.Center)
.height(40)
.width("100%")
}.width("100%")
}
}.onDidScroll((scrollOffset: number, scrollState: ScrollState) => {
this.onScroll(scrollOffset, scrollState)
// 惯性滚动时 达到底部后 回弹过程中
if (scrollState === ScrollState.Fling) {
// 累加持续滚动的距离
this.yStartFling += scrollOffset
// 已经滑到底部了 且不是在加载中
if (this.scroller.isAtEnd() && !this.isLoading) {
// 更改标记 让“加载中”布局展示
this.isShowLoadingWhenTouch = true
// 计算当前“加载中”布局的偏移量,this.dragY为当前的偏移量;之所减去yStartFling是因为偏移量往下是正方向,页面往上拖scrollState为正方向,减去yStartFling才是最终的偏移量
const changeY = this.dragY - this.yStartFling
// 赋值偏移量,边界处理,值为正负的loadMoreViewHeight
this.dragY = changeY > this.loadMoreViewHeight ? this.loadMoreViewHeight :
changeY < -this.loadMoreViewHeight ? -this.loadMoreViewHeight : changeY
// 判断上滑,且list跟随手势滑动
if (this.yStartFling > 0) {
// 如果惯性滚动的距离小于0
if (changeY < 0) {
// 判断不是在加载更多中
if (!this.isLoading && !this.isNoMoreData) {
// 执行加载更多的回调
this.loadMore()
this.isLoading = true
// 执行动画 让整个加载布局全部展示
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
}, () => {
this.dragY = 0
})
}
}
}
} else {
// 隐藏加载更多的布局
this.isShowLoadingWhenTouch = false
this.yStartFling = 0
this.dragY = this.loadMoreViewHeight
}
}
})
.scrollBar(BarState.Off)
.width("100%")
.height("100%")
}.onRefreshing(() => {
// if(!this.isRefreshing){
this.reFresh()
this.isNoMoreData = false
// }
}).width("100%")
.height("100%")
// 底部加载更多的布局
if (this.isShowLoadingWhenTouch && this.isNeedLoadMore && !this.isNoMoreData) {
Row() {
this.LoadFooter()
}.width("100%")
.height(this.loadMoreViewHeight)
.translate({ y: this.dragY })
}
}.onTouch((event: TouchEvent) => {
switch (event.type) {
case TouchType.Down:
// 标记起始位置的坐标
this.yStart = event.touches[0].y;
break
case TouchType.Move:
// 标记当前位置的坐标
const yEnd = event.touches[0].y; // 手指离开屏幕的纵坐标
// 已经滑到底部了
if (this.scroller.isAtEnd() && !this.isLoading) {
// 展示底部局部
this.isShowLoadingWhenTouch = true
// 计算加载更多布局的偏移量;*0.6是用来调手感的,
const changeY = (yEnd - this.yStart) * 0.6 + this.loadMoreViewHeight
// 偏移量边界处理
this.dragY = changeY < -this.loadMoreViewHeight ? -this.loadMoreViewHeight : changeY
console.debug(`this.dragY = ${this.dragY}`)
// 判断上滑,且list跟随手势滑动
if (yEnd <= this.yStart) {
// 标记手势Up时,是否要回调加载更多
if (changeY < this.loadMoreViewHeight) {
this.isLoadMoreWhenUp = true
} else {
this.isLoadMoreWhenUp = false
}
} else {
// 如果往下滑,且超过了起始位置,则置回起始位置,同时更新手势的起始位置
if (this.dragY >= this.loadMoreViewHeight) {
this.dragY = this.loadMoreViewHeight
this.isLoadMoreWhenUp = false
this.yStart = yEnd
} else {
// 同上面yEnd <= this.yStart的逻辑
if (changeY < this.loadMoreViewHeight) {
this.isLoadMoreWhenUp = true
} else {
this.isLoadMoreWhenUp = false
}
}
}
} else {
// 没到底部 就置回起始状态
this.isShowLoadingWhenTouch = false
this.yStart = yEnd
if (!this.scroller.isAtEnd()) {
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
}, () => {
this.dragY = this.loadMoreViewHeight
})
}
}
break
case TouchType.Up:
// 手指up时,如果需要回调事件就回调,同时加载中布局全部展示,否则置回起始状态
if (this.isLoadMoreWhenUp) {
if (!this.isRefreshing && !this.isLoading && !this.isNoMoreData) {
this.loadMore()
this.isLoading = true
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
}, () => {
this.dragY = 0
})
} else {
this.dragY = this.loadMoreViewHeight
this.isShowLoadingWhenTouch = false
}
} else {
animateTo({
duration: 100,
curve: Curve.EaseOut,
playMode: PlayMode.Normal,
onFinish: () => {
this.isShowLoadingWhenTouch = false
}
}, () => {
this.dragY = this.loadMoreViewHeight
})
}
break
}
})
.width("100%")
.height("100%")
}
finishRefresh() {
this.isRefreshing = false
}
finishLoadMore() {
this.dragY = this.loadMoreViewHeight
this.isLoading = false
}
// 没有更多数据
noMoreData(value: boolean) {
this.isNoMoreData = value
}
// 没有更多数据
needLoadMore(value: boolean) {
this.isNeedLoadMore = value
}
@Builder
buildBody() {
Text('未设置body')
}
@Builder
RefreshHeader() {
Column() {
RefreshHeaderLayout()
}
}
@Builder
LoadFooter() {
Column() {
LoadingMoreLayout()
}.width("100%")
.backgroundColor(Color.White)
}
}
export class CppPullToRefreshController {
private cppRefreshView: CppPullToRefresh | undefined
bind(cppRefreshView: CppPullToRefresh) {
this.cppRefreshView = cppRefreshView
}
needLoadMore(value: boolean) {
this.cppRefreshView?.needLoadMore(value)
}
finishLoadMore() {
this.cppRefreshView?.finishLoadMore()
}
finishRefresh() {
this.cppRefreshView?.finishRefresh()
}
// 没有更多
noMoreData(value: boolean) {
this.cppRefreshView?.noMoreData(value)
}
}
LoadingMoreLayout, RefreshHeaderLayout是自定义的头布局和底部布局,这个根据自己需要写一下即可,就不贴代码了。
贴下使用代码:
js
复制代码
CppPullToRefresh({
viewController:this.viewController,
// isNeedLoadMore:true,
// isNeedNoMoreData:true,
body: (): void => {
this.buildBody()
},
reFresh:()=>{
this.petHistoryRecords(true)
},
loadMore:()=>{
this.petHistoryRecords()
}
}) .width("100%")
@Builder
buildBody() {
ListItem() {}
or
LazyForEach(){
ListItem() {
}
}
}
因为控件内部是List,所以只能传ListItem/ListGroupItem的子节点,如果只是对一个布局做刷新,可以传一个布局用ListItem包起来即可。
四、写在最后
最后没有最后,问下,你们开始适配鸿蒙了吗?
如有侵权请联系站点删除!
技术合作服务热线,欢迎来电咨询!