其他
好用的HarmonyOS Next 横向、纵向下拉刷新控件
本文作者
作者:钟睿
链接:
https://juejin.cn/post/7395866502716702755
本文由作者授权发布。
前言
(源码地址和使用教程链接在文末)
1.无入侵性,不需要传数据源
2.不限制组件,支持任意布局(List,Grid,Web,Scroll,Text,Row,Column等布局)
3.支持header和footer个性化视图扩展(支持Lottie动画)
4.支持垂直列表和横向列表的刷新和加载
垂直列表
垂直List列表刷新效果:
垂直Grid列表刷新效果:
下拉打开其他页面:
自动刷新:
Web视图刷新效果:
自定义动画刷新效果:
Lottie动画刷新效果:
横向列表刷新:
三种横向模式header效果图(footer同理)
header正常横向
header宽度固定,高度撑满
header宽度撑满,高度固定
和垂直列表布局方式一致
header宽度撑满,高度固定
和垂直列表布局方式一致
缺省页设置(加载中,空数据,加载失败,无网络)
第1步通过自定义布局构建下拉刷新视图结构
/*这里的header,content,footer视图,全部由外部传入*/
//header视图
@BuilderParam headerView: () => void
//内容视图
@BuilderParam contentView: () => void
//footer视图
@BuilderParam loadView: () => void
build() {
this.headerAndContent()
}
/*在build函数内直接定义多个根视图编译会报错,这里使用@Builder绕过检查*/
@Builder
private headerAndContent() {
//header视图(垂直列表时,设置宽度撑满,高度自适应)
Stack(){
this.headerView()
}.width("100%")
//内容视图
Stack(){
this.contentView()
}.width("100%").height("100%")
//footer视图(垂直列表时,设置宽度撑满,高度自适应)
Stack(){
this.loadView()
}.width("100%")
}
private sizeResult: SizeResult = { width: 0, height: 0 }
//header视图高度
private headerHeight=0
//视图测量
onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult {
//selfLayoutInfo:父组件布局信息
//children :子组件布局信息
//constraint :父组件constraint信息
//测量子组件
const headerResult = children[0].measure(constraint)
const contentResult = children[1].measure(constraint)
const footerResult = children[2].measure(constraint)
//记录header视图高度,触发刷新时,动画回弹需要
this.headerHeight=headerResult.height;
//将内容视图区域的宽高设置为当前组件宽高
this.sizeResult.width = contentResult.width;
this.sizeResult.height = contentResult.height;
//返回组件尺寸信息
return this.sizeResult
}
//视图布局
onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void {
const childHeader = children[0]
//设置header视图向上偏移自身高度
childHeader.layout({ y: -childHeader.measureResult.height })
const childContent = children[1]
//content视图不需要做偏移
childContent.layout({})
const childFooter = children[2]
//footer需要向下偏移,偏移量为内容视图高度
childFooter.layout({ y: this.sizeResult.height })
}
第2步实现下拉
//header和content视图的偏移量(视图回弹动画需要)
totalOffsetY: number = 0
//header和content视图的偏移量
@State currentOffsetY: number = 0
//记录上一次拖动手势的偏移量
preOffsetY = 0;
//header和content视图设置offset属性
.offset({ y: this.currentOffsetY })
//content视图
Stack() {
this.contentView()
}
.offset({ y: this.currentOffsetY })
.width("100%")
.height("100%")
.parallelGesture(PanGesture(new PanGestureOptions({ direction: PanDirection.Vertical }))
.onActionStart((event: GestureEvent) => {
//Pan手势识别成功回调
//记录偏移量
this.preOffsetY = event.offsetY
}).onActionUpdate((event: GestureEvent) => {
//Pan手势移动过程中回调
//新增偏移量=(当前手势偏移量-上一次手势偏移量)*0.5阻尼系数
//视图总偏移量=新增偏移量+当前视图偏移量
this.currentOffsetY = this.currentOffsetY + (event.offsetY - this.preOffsetY)*0.5
if(this.currentOffsetY<0){
//上拉视图时,不能让内容超出组件范围
this.currentOffsetY=0
}
this.totalOffsetY=this.currentOffsetY
this.preOffsetY = event.offsetY
if(this.currentOffsetY<100){
//小于100vp时显示"下拉刷新"相关视图逻辑
}else{
//下拉距离超过100vp显示"释放刷新"相关视图逻辑
}
}).onActionEnd((event: GestureEvent)=>{
//Pan手势识别成功,手指抬起后触发回调
}).onActionCancel(()=>{
//Pan手势识别成功,接收到触摸取消事件触发回调
//在窗口失焦的时候会触发
}))
第3步实现视图回弹
import animator from '@ohos.animator'
private animOption:AnimatorOptions={
duration: 250,
easing: "fast-out-linear-in",
delay: 0,
fill: "forwards",
direction: "normal",
iterations: 1,
begin: 0,
end: 1
}
private anim: AnimatorResult = animator.create(this.animOption);
private animPause=false;
aboutToAppear(): void {
this.anim.onFrame = (progress: number) => {
if(this.animPause){
//取消动画时,progress会变成1,如果不return this.currentOffsetY会立刻变成0
return
}
//通过总偏移量和动画执行进度,计算出当前视图偏移量
this.currentOffsetY = this.totalOffsetY * (1 - progress)
}
this.anim.onFinish=()=>{
//动画完成时,总偏移量等于当前视图偏移量
this.totalOffsetY=this.currentOffsetY
}
this.anim.onCancel=()=>{
//动画取消时,总偏移量等于当前视图偏移量
this.totalOffsetY=this.currentOffsetY
}
}
//给content视图设置onTouch事件
.onTouch((event: TouchEvent) => {
const type = event.type
if(type==TouchType.Down){
//下拉距离大于0时,触摸content视图暂停回弹动画
if(this.currentOffsetY>0){
this.animPause=true
this.anim.cancel()
}
}else if (type == TouchType.Up || type == TouchType.Cancel) {
//下拉距离大于0时,触摸content视图暂停回弹动画
if(this.currentOffsetY>0){
this.animPause=false
//执行回弹动画
this.anim.play()
}
}
})
第4步触发刷新,显示header刷新视图
外部设置开始刷新的回调
public onRefresh: () => void = () => {
}
//释放触发刷新动作
.onTouch((event: TouchEvent) => {
const type = event.type
if (type == TouchType.Down) {
if (this.currentOffsetY > 0) {
this.animPause = true
this.anim.cancel()
}
} else if (type == TouchType.Up || type == TouchType.Cancel) {
this.animPause = false
if (this.currentOffsetY > 100) {
//此时修改header视图,显示正在刷新中的视图
//如果下拉高度达到刷新条件,释放时触发刷新操作
this.onRefresh()
/*计算回弹至header高度的progress进度*/
this.animOption.end = (this.currentOffsetY - this.headerHeight) / this.currentOffsetY
this.anim.reset(this.animOption)
this.anim.play()
} else if (this.currentOffsetY > 0) {
//执行回弹动画
this.animOption.end = 1
this.anim.reset(this.animOption)
this.anim.play()
}
}
})
第5步构造controller,通知组件内部刷新完成
PullToRefreshLayout({
/*设置控制器*/
controller: this.controller,
/*内容视图*/
contentView: () => {
this.contentView()
},
/*触发刷新*/
onRefresh: () => {
setTimeout(() => {
//通知刷新成功
this.controller.refreshComplete(true)
}, 1000)
}
}).width("100%").height("100%").clip(true)
export class RefreshController {
/*刷新完成,true:成功,false:失败*/
refreshComplete: (isSuccess: boolean) => void = (isSuccess: boolean) => {
}
}
public controller: RefreshController = new RefreshController()
aboutToAppear(): void {
/*通过参数通知刷新结果*/
this.controller.refreshComplete = (isSuccess: boolean) => {
if(isSuccess){
//处理刷新成功的逻辑
}else if(){
//处理刷新失败的逻辑
}
//header视图提示"刷新成功"或"刷新失败"
//随后通过动画回弹隐藏header视图
this.animOption.end = 1
this.anim.reset(this.animOption)
this.anim.play()
}}
第6步解决上滑再下拉,header会往下偏移问题
PullToRefreshLayout({
/*设置控制器*/
controller: this.controller,
/*内容视图*/
contentView: () => {
this.contentView()
},
/*触发刷新*/
onRefresh: () => {
setTimeout(() => {
//通知刷新成功
this.controller.refreshSuccess()
}, 1000)
},
/*是否可以下拉*/
onCanPullRefresh: () => {
if (!this.scroller.currentOffset()) {
/*处理无数据,为空的情况*/
return true
}
//如果列表到顶,返回true,表示可以下拉,返回false,表示无法下拉
return this.scroller.currentOffset().yOffset <= 0
}
}).width("100%").height("100%").clip(true)
/*是否可以下拉,默认为true*/
public onCanPullRefresh: () => boolean = () => true
.onActionUpdate((event: GestureEvent) => {
//如果不能下拉,则header视图不发生偏移
if(!this.onCanPullRefresh()){
return
}
//Pan手势移动过程中回调
//新增偏移量=(当前手势偏移量-上一次手势偏移量)*0.5阻尼系数
//视图总偏移量=新增偏移量+当前视图偏移量
this.currentOffsetY = this.currentOffsetY + (event.offsetY - this.preOffsetY)*0.5
if(this.currentOffsetY<0){
//上拉视图时,不能让内容超出组件范围
this.currentOffsetY=0
}
this.totalOffsetY=this.currentOffsetY
this.preOffsetY = event.offsetY
if(this.currentOffsetY<100){
//小于100vp时显示"下拉刷新"相关视图逻辑
}else{
//下拉距离超过100vp显示"释放刷新"相关视图逻辑
}})
第7步解决下拉再上拉,列表内容会滑动问题
public scroller: Scroller | undefined = undefined
.onActionUpdate((event: GestureEvent) => {
if(!this.onCanPullRefresh()){
return
}
//存在下拉距离的情况下,向上滑动视图
if (this.currentOffsetY > 0 && this.preOffsetY>event.offsetY) {
/*如果下拉再上拉,不让列表滑动*/
if (this.scroller) {
this.scroller.scrollTo({ yOffset: 0xOffset: this.scroller.currentOffset()?.xOffset ?? 0 })
}}
scroller: Scroller = new Scroller()
PullToRefreshLayout({
//设置内容列表的滚动控制器
scroller: this.scroller,
controller: this.controller,
/*内容视图*/
contentView: () => {
this.contentView()
}
})
.width("100%")
.height("100%")
.clip(true)
@Builder
contentView() {
List({ scroller: this.scroller }) {
}.width("100%").height("100%")
.edgeEffect(EdgeEffect.None) }
v1使用教程:
https://ohpm.openharmony.cn/#/cn/detail/@zhongrui%2Fpull_to_refresh
v2使用教程:
https://ohpm.openharmony.cn/#/cn/detail/@zhongrui%2Fpull_to_refresh_v2
项目源码
https://gitee.com/zhongrui_developer/PullToRefresh
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!