页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的触摸事件,在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。
实现思路
以下拉刷新为例,其实现主要分成三步:
- 监听手指按下事件,记录其初始位置的值。
- 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于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,
}