自定义组件之<九>自定义下拉刷新上拉加载(RefreshLayout)
2024-12-31 10:27:24
29次阅读
0个评论
9.8:自定义下拉刷新上拉加载(RefreshLayout)
自定义完毕下拉刷新后接着是给其实现上拉加载,实现顺序和下拉刷新相反的,运行效果如下图所示:
9.8.1:RefreshLayout完整样例
下拉刷新和上拉加载的实现是一致的,笔者不再详述实现过程了,源码如下所示:
export class Constant {
static readonly REFRESH_PULL_TO_REFRESH = "下拉刷新";
static readonly REFRESH_FREE_TO_REFRESH = "释放立即刷新";
static readonly REFRESH_REFRESHING = "正在刷新";
static readonly REFRESH_SUCCESS = "刷新成功";
static readonly REFRESH_PULL_TO_LOAD_MORE = "上拉加载更多";
}
@ComponentV2
export struct RefreshLayout {
@BuilderParam itemLayout?: (item: Object, index: number) => void;
@Param refreshing: boolean = false
@Event $refreshing: (refreshing: boolean) => void = (refreshing: boolean) => {}
@Param loadMore: boolean = false
@Event $loadMore: (loadMore: boolean) => void = (loadMore: boolean) => {}
@Param dataSet: Array<Object> = new Array<Object>()
@Param onRefresh: () => void = () => {}
@Param onLoadMore: () => void = () => {}
@Param onStatusChanged: (status: RefreshStatus) => void = (status: RefreshStatus) => {}
private headHeight: number = 55;
private footHeight: number = 55;
private lastX: number = 0;
private lastY: number = 0;
private downY: number = 0;
private flingFactor: number = 0.75;
private touchSlop: number = 2;
private offsetStep: number = 10;
private intervalTime: number = 20;
private listScrollable: boolean = true;
private dragging: boolean = false;
private refreshStatus: RefreshStatus = RefreshStatus.Inactive;
private listScroller: Scroller = new Scroller();
@Local offsetY: number = -this.headHeight;
@Local refreshHeadIcon: Resource = $r("app.media.icon_refresh_down");
@Local refreshFootIcon: Resource = $r("app.media.icon_refresh_up");
@Local refreshFootText: string = Constant.REFRESH_PULL_TO_LOAD_MORE;
@Local refreshHeadText: string = Constant.REFRESH_PULL_TO_REFRESH;
@Local refreshContentH: number = 0;
@Local touchEnabled: boolean = true;
@Monitor("refreshing")
notifyRefreshingChanged(monitor: IMonitor) {
if (this.refreshing) {
this.showRefreshingStatus();
} else {
this.finishRefresh();
}
}
@Monitor("loadMore")
notifyLoadMoreChanged(monitor: IMonitor) {
if (this.loadMore) {
this.showLoadMoreStatus();
} else {
this.finishLoadMore();
}
}
@Builder headLayout() {
Row() {
Blank()
Image(this.refreshHeadIcon)
.width(30)
.aspectRatio(1)
.objectFit(ImageFit.Contain)
Text(this.refreshHeadText)
.fontSize(16)
.width(150)
.textAlign(TextAlign.Center)
Blank()
}
.width("100%")
.height(this.headHeight)
.backgroundColor("#44bbccaa")
.position({
x: 0,
y: this.offsetY
})
}
@Builder footLayout() {
Row() {
Blank()
Image(this.refreshFootIcon)
.width(30)
.aspectRatio(1)
.objectFit(ImageFit.Contain)
Text(this.refreshFootText)
.fontSize(16)
.width(150)
.textAlign(TextAlign.Center)
Blank()
}
.width("100%")
.height(this.footHeight)
.backgroundColor("#33bbccaa")
.position({
x: 0,
y: this.offsetY + this.headHeight + this.refreshContentH
})
}
build() {
Column() {
this.headLayout()
List({ space: 20, initialIndex: 0 , scroller: this.listScroller}) {
if (this.dataSet) {
ForEach(this.dataSet, (item: Object, index: number) => {
ListItem() {
if (this.itemLayout) {
this.itemLayout(item, index)
}
}
.width("100%")
}, (item: Object, index: number) => item.toString())
}
}
.width("100%")
.height(this.refreshContentH)
.edgeEffect(EdgeEffect.None)
.enabled(this.touchEnabled)
.position({
x: 0,
y: this.offsetY + this.headHeight
})
.onScrollFrameBegin((offset: number, state: ScrollState) => {
offset = this.listScrollable ? offset : 0;
return {offsetRemain: offset}
})
this.footLayout()
}
.width("100%")
.height("100%")
.enabled(this.touchEnabled)
.onAreaChange((oldArea, newAre) => {
this.logD("刷新内容高度:" + newAre.height)
this.refreshContentH = newAre.height as number;
})
.clip(true)
.onTouch((event) => {
if (event.touches.length != 1) {
this.logD("TOUCHES LENGTH INVALID: " + JSON.stringify(event.touches))
return;
}
if (this.refreshStatus == RefreshStatus.Refresh || this.refreshStatus == RefreshStatus.Done) {
this.logD("REFRESH STATUS: " + this.refreshStatus);
return;
}
switch (event.type) {
case TouchType.Down:
this.onTouchDown(event);
break;
case TouchType.Move:
this.onTouchMove(event);
break;
case TouchType.Up:
case TouchType.Cancel:
this.onTouchUp(event);
break;
}
})
}
aboutToAppear() {
if (this.refreshing) {
this.showRefreshingStatus();
}
}
private setRefreshStatus(status: RefreshStatus) {
this.refreshStatus = status;
this.touchEnabled = (status != RefreshStatus.Refresh && status != RefreshStatus.Done);
this.notifyStatusChanged();
}
private onTouchDown(event: TouchEvent) {
this.lastX = event.touches[0].x;
this.lastY = event.touches[0].y;
this.downY = this.lastY;
this.dragging = false;
this.listScrollable = true;
// this.logD("Touch DOWN: " + event.touches[0].x.toFixed(2) + " x " + event.touches[0].y.toFixed(2) + ", offset: " + this.offsetY);
}
private canPullToRefresh(): boolean {
return this.listScroller.currentOffset().yOffset == 0;
}
private canLoadToRefresh(): boolean {
return this.listScroller.isAtEnd();
}
private onTouchMove(event: TouchEvent) {
let currentX = event.touches[0].x;
let currentY = event.touches[0].y;
let deltaX = currentX - this.lastX;
let deltaY = currentY - this.lastY;
let canRefresh = this.canPullToRefresh()
let canLoadMore = this.canLoadToRefresh()
if (canRefresh || canLoadMore) {
if (this.dragging) {
this.logD("offsetY: " + this.offsetY + ", head: " + (-this.headHeight));
if (deltaY < 0) {
// 手指向上滑动
if (canLoadMore) {
this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
this.lastX = currentX;
this.lastY = currentY;
} else {
if (this.offsetY > -this.headHeight) {
// this.logD("手指向上拖动还未到达临界值,不让 list 滚动")
this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
this.lastX = currentX;
this.lastY = currentY;
this.listScrollable = false;
} else {
// this.logD("手指向上拖动到达临界值了,开始让 list 滚动")
this.offsetY = -this.headHeight;
this.listScrollable = true;
this.lastX = currentX;
this.lastY = currentY;
this.downY = this.lastY;
}
}
} else {
// 手指向下滑动
if (canLoadMore) {
if (this.offsetY < -this.headHeight) {
// this.logD("手指向下拖动还未到达临界值,不让 list 滚动")
this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
this.lastX = currentX;
this.lastY = currentY;
this.listScrollable = false;
} else {
// this.logD("手指向下拖动到达临界值了,开始让 list 滚动")
this.offsetY = -this.headHeight;
this.listScrollable = true;
this.dragging = false;
this.lastX = currentX;
this.lastY = currentY;
this.downY = this.lastY;
}
} else {
if (this.listScroller.currentOffset().yOffset == 0) {
this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
}
this.lastX = currentX;
this.lastY = currentY;
}
}
} else {
if (Math.abs(deltaX) < Math.abs(deltaY) && Math.abs(deltaY) > this.touchSlop) {
if (deltaY > 0 && canRefresh) {
this.dragging = true;
this.listScrollable = false
this.lastX = currentX;
this.lastY = currentY;
this.logD("Touch MOVE: 手指向下滑动,达到了拖动条件")
}
if (deltaY < 0 && canLoadMore) {
this.dragging = true;
this.listScrollable = false;
this.lastX = currentX;
this.lastY = currentY;
this.logD("Touch MOVE: 手指向上滑动,达到了拖动条件")
}
}
}
}
if(this.dragging) {
if (currentY >= this.downY) {
if (this.offsetY >= 0 || (this.headHeight - Math.abs(this.offsetY)) > this.headHeight * 4 / 5) {
this.refreshHeadText = Constant.REFRESH_FREE_TO_REFRESH;
this.refreshHeadIcon = $r("app.media.icon_refresh_up");
this.setRefreshStatus(RefreshStatus.OverDrag);
} else {
this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;
this.refreshHeadIcon = $r("app.media.icon_refresh_down");
this.setRefreshStatus(RefreshStatus.Drag);
}
} else {
if ((Math.abs(this.offsetY) - this.footHeight) > this.footHeight * 4 / 5 ) {
this.refreshFootText = Constant.REFRESH_FREE_TO_REFRESH;
this.refreshFootIcon = $r("app.media.icon_refresh_down");
this.setRefreshStatus(RefreshStatus.OverDrag);
} else {
this.refreshFootText = Constant.REFRESH_PULL_TO_LOAD_MORE;
this.refreshFootIcon = $r("app.media.icon_refresh_up");
this.setRefreshStatus(RefreshStatus.Drag);
}
}
}
}
private onTouchUp(event: TouchEvent) {
let currentY = event.touches[0].y;
if (this.dragging) {
if (currentY >= this.downY) {
// 手指最终向下滑动
if (this.offsetY >= 0 || (this.headHeight - Math.abs(this.offsetY)) > this.headHeight * 4 / 5) {
this.logD("Touch UP: 最终达到下拉刷新条件")
this.refreshHeadIcon = $r("app.media.icon_refresh_loading");
this.refreshHeadText = Constant.REFRESH_REFRESHING;
this.setRefreshStatus(RefreshStatus.Refresh)
this.scrollToTop();
this.notifyRefreshStarted();
} else {
this.logD("Touch UP: 最终未达到下拉刷新条件")
this.refreshHeadIcon = $r("app.media.icon_refresh_down");
this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;
this.setRefreshStatus(RefreshStatus.Drag);
this.scrollByTop();
}
} else {
// 手指最终向上滑动
if ((Math.abs(this.offsetY) - this.footHeight) > this.footHeight * 4 / 5 ) {
this.logD("Touch UP: 最终达到上拉加载更多刷新条件")
this.refreshFootIcon = $r("app.media.icon_refresh_loading");
this.refreshFootText = Constant.REFRESH_REFRESHING;
this.setRefreshStatus(RefreshStatus.Refresh);
this.scrollToBottom();
this.notifyLoadMoreStarted();
} else {
this.logD("Touch UP: 最终未达到上拉加载更多刷新条件")
this.refreshFootIcon = $r("app.media.icon_refresh_up");
this.refreshFootText = Constant.REFRESH_PULL_TO_LOAD_MORE;
this.setRefreshStatus(RefreshStatus.Drag);
this.scrollByBottom();
}
}
} else {
this.logD("Touch UP: 没有触发拖动条件")
}
this.listScrollable = true;
this.dragging = false
}
private showRefreshingStatus() {
this.offsetY = 0;
this.refreshHeadIcon = $r("app.media.icon_refresh_loading");
this.refreshHeadText = Constant.REFRESH_REFRESHING;
this.setRefreshStatus(RefreshStatus.Refresh);
}
private showLoadMoreStatus() {
this.offsetY = -(this.footHeight + this.headHeight);
this.refreshFootIcon = $r("app.media.icon_refresh_loading");
this.refreshFootText = Constant.REFRESH_REFRESHING;
this.setRefreshStatus(RefreshStatus.Refresh);
}
private updateRefreshStatus(status: RefreshStatus) {
this.refreshStatus = status;
this.touchEnabled = (status != RefreshStatus.Refresh && status != RefreshStatus.Done);
this.notifyStatusChanged();
}
private scrollToBottom() {
this.offsetY = -(this.headHeight + this.footHeight);
}
private scrollByBottom() {
if (this.offsetY != -this.headHeight) {
this.logD("scrollByBottom() start, offsetY: " + this.offsetY)
let intervalId = setInterval(() => {
if(this.offsetY >= -this.headHeight) {
this.resetRefreshStatus();
clearInterval(intervalId);
this.logD("scrollByBottom() finish, offsetY: " + this.offsetY)
} else {
this.offsetY = ((this.offsetY + this.offsetStep) > -this.headHeight) ? (-this.headHeight) : (this.offsetY + this.offsetStep);
}
}, this.intervalTime);
} else {
this.logD("scrollByBottom(): already scrolled to bottom");
}
}
private scrollToTop() {
this.offsetY = 0;
}
private scrollByTop() {
if (this.offsetY != -this.headHeight) {
this.logD("scrollByTop() start, offsetY: " + this.offsetY);
let intervalId = setInterval(() => {
if(this.offsetY <= -this.headHeight) {
this.resetRefreshStatus();
clearInterval(intervalId);
this.logD("scrollByTop() finish, offsetY: " + this.offsetY);
} else {
this.offsetY = ((this.offsetY - this.offsetStep) < -this.headHeight) ? (-this.headHeight) : (this.offsetY - this.offsetStep);
}
}, this.intervalTime);
} else {
this.logD("scrollByTop(): already scrolled to top")
}
}
private resetRefreshStatus() {
this.refreshStatus = RefreshStatus.Inactive;
this.refreshHeadIcon = $r("app.media.icon_refresh_down");
this.refreshFootIcon = $r("app.media.icon_refresh_up");
this.offsetY = -this.headHeight;
this.refreshFootText = Constant.REFRESH_PULL_TO_LOAD_MORE;
this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;
this.touchEnabled = true;
}
private finishRefresh(): void {
this.refreshHeadText = Constant.REFRESH_SUCCESS;
this.refreshHeadIcon = $r("app.media.icon_refresh_success");
this.setRefreshStatus(RefreshStatus.Done);
setTimeout(() => {
this.scrollByTop();
}, 1500);
}
private finishLoadMore(): void {
this.refreshFootText = Constant.REFRESH_SUCCESS;
this.refreshFootIcon = $r("app.media.icon_refresh_success");
this.setRefreshStatus(RefreshStatus.Done);
setTimeout(() => {
this.scrollByBottom();
}, 1500);
}
private notifyStatusChanged() {
if (this.onStatusChanged) {
this.onStatusChanged(this.refreshStatus);
}
}
private notifyRefreshStarted() {
if (this.onRefresh) {
this.onRefresh();
}
}
private notifyLoadMoreStarted() {
if (this.onLoadMore) {
this.onLoadMore();
}
}
private logD(msg: string) {
console.log(msg + ", dragging: " + this.dragging + ", listScrollable: " + this.listScrollable)
}
}
9.8.2:完整样例
笔者实现的 RefreshLayout 很简单,使用方式也和 Refresh 的用法保持了一致,唯一需要注意的就是想要自己实现 List 的每一个 item 局部,样例代码如下所示:
import { RefreshLayout } from './refresh_layout';
@Entry @ComponentV2 struct Page_07_loadmore {
@Local dataSet: Array<string> = [];
@Local refreshing: boolean = false;
@Local loadMore: boolean = false;
@Builder itemLayout(item: object, index: number) {
Text("item:" + item + ", index: " + index)
.width('80%')
.height(100)
.margin({top: 10})
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor('#bbccaa')
.borderRadius(10)
}
build() {
Column() {
Row({space: 10}) {
Button("下拉刷新")
.onClick(() => {
this.refreshing = true;
})
Button("下拉刷新结束")
.onClick(() => {
this.refreshing = false;
})
Button("上拉刷新")
.onClick(() => {
this.loadMore = true;
})
Button("上拉刷新结束")
.onClick(() => {
this.loadMore = false;
})
}
.width("100%")
.height(50)
Column() {
RefreshLayout({
refreshing: this.refreshing!!,
loadMore: this.loadMore!!,
dataSet: this.dataSet,
itemLayout: (item: Object, index: number) => {
this.itemLayout(item, index);
},
onRefresh: () => {
this.refreshing = true;
this.doRefresh();
},
onLoadMore: () => {
this.loadMore = true;
this.doLoadMore();
},
onStatusChanged: (status) => {
console.log("current status: " + status);
}
})
}
.clip(true)
.width("100%")
.layoutWeight(1)
}
.width("100%")
.width('100%')
}
aboutToAppear() {
this.initDataSet((Math.random() * 100), 10);
}
private doRefresh() {
setTimeout(() => {
console.log("finish refresh")
this.initDataSet((Math.random() * 100), 10);
this.refreshing = false;
}, 2500);
}
private doLoadMore() {
setTimeout(() => {
console.log("finish loadMore");
let count = this.dataSet.length;
let dataSet = new Array<string>();
for (let i = 0; i < count; i++) {
dataSet.push(this.dataSet[i]);
}
let rand = (Math.random() * 100);
for (let i = 0; i < 10; i++) {
dataSet.push((i + rand).toFixed(2));
}
this.dataSet = dataSet;
this.loadMore = false;
}, 2500);
}
private initDataSet(start: number, count: number) {
let dataSet = new Array<string>();
for (let i = 0; i < count; i++) {
dataSet.push((i + start).toFixed(2));
}
this.dataSet = dataSet;
}
}
备注
作者:灰太狼
來源:坚果派
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。否则追究相关责任。
00
- 0回答
- 1粉丝
- 0关注
相关话题
- 自定义组件之<八>自定义下拉刷新(RefreshList)
- 自定义组件之<七>自定义组件之插槽(slot)
- 自定义组件之<二>自定义圆环(Ring)
- 自定义组件之<六>自定义饼状图(PieChart)
- 自定义组件之<四>自定义对话框(Dialog)
- 自定义组件之<三>自定义标题栏(TitleBar)
- 自定义组件之<五>自定义对话框(PromptAction)
- 自定义组件之<十>发布开源库
- 如何加载和使用自定义字体
- 自定义组件之<一>组件语法和生命周期
- @ComponentV2装饰器:自定义组件
- 鸿蒙自定义组件生命周期
- 页面和自定义组件生命周期
- HarmonyOS NEXT实战:自定义封装多种样式导航栏组件
- HarmonyOS应用开发实战:半天实现知乎日报项目(七、知乎日报List列表下拉刷新及上滑加载更多分页的实现)