鸿蒙Next开发:可自定义扩展下拉刷新+上拉加载(纵向横向) ...

打印 上一主题 下一主题

主题 919|帖子 919|积分 2757

前言

在移动端下拉刷新和上拉加载属于高频使用的功能,现在官方提供的Refresh组件自定义效果有限,很难满足各产业品个性化定制的要求。下面我们从0到1打造一个无入侵性、可自定义扩展的下拉刷新组件
目标特点

1.无入侵性,不必要传数据源
2.不限制组件,支持恣意结构(List,Grid,Web,Scroll,Text,Row,Column等结构)
3.支持header和footer个性化视图扩展(支持Lottie动画)
4.支持垂直列表和横向列表的刷新和加载
5.支持下拉(大概上拉)打开其他页面
效果图

垂直列表

垂直List列表刷新效果垂直Grid列表刷新效果下拉打开其他页面主动刷新
Web视图刷新效果自定义动画刷新效果Lottie动画刷新效果横向列表刷新
三种横向模式header效果图(footer同理)

header正常横向
header宽度固定,高度撑满header结构逆时针旋转90°
header宽度撑满,高度固定
和垂直列表结构方式一致header结构顺时针旋转90°
header宽度撑满,高度固定
和垂直列表结构方式一致
第1步通过自定义结构构建下拉刷新视图结构


  1. /*这里的header,content,footer视图,全部由外部传入*/
  2. //header视图
  3. @BuilderParam headerView: () => void
  4. //内容视图
  5. @BuilderParam contentView: () => void
  6. //footer视图
  7. @BuilderParam loadView: () => void
  8. build() {
  9.    this.headerAndContent()
  10. }
  11. /*在build函数内直接定义多个根视图编译会报错,这里使用@Builder绕过检查*/
  12. @Builder
  13. private headerAndContent() {
  14.   //header视图(垂直列表时,设置宽度撑满,高度自适应)
  15.   Stack(){
  16.       this.headerView()
  17.   }.width("100%")
  18.   
  19.   //内容视图
  20.   Stack(){
  21.       this.contentView()
  22.   }.width("100%").height("100%")
  23.   
  24.   //footer视图(垂直列表时,设置宽度撑满,高度自适应)
  25.   Stack(){
  26.       this.loadView()
  27.   }.width("100%")
  28. }
复制代码
现在这种结构会出现视图重叠,还无法实现上图的结构,这个时候借助onMeasureSize()和 onPlaceChildren()实现对三个结构的测量和摆放
重要处理header视图和footer视图的偏移, header向上偏移,偏移量为自身高度的负数,footer向下偏移,偏移量为内容视图高度
  1. private sizeResult: SizeResult = { width: 0, height: 0 }
  2. //header视图高度
  3. private headerHeight=0
  4. //视图测量
  5. onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult {
  6.     //selfLayoutInfo:父组件布局信息
  7.     //children      :子组件布局信息
  8.     //constraint    :父组件constraint信息
  9.    
  10.     //测量子组件
  11.     const headerResult  = children[0].measure(constraint)
  12.     const contentResult = children[1].measure(constraint)
  13.     const footerResult  = children[2].measure(constraint)
  14.    
  15.     //记录header视图高度,触发刷新时,动画回弹需要
  16.     this.headerHeight=headerResult.height;
  17.    
  18.     //将内容视图区域的宽高设置为当前组件宽高
  19.     this.sizeResult.width = contentResult.width;
  20.     this.sizeResult.height = contentResult.height;
  21.         
  22.     //返回组件尺寸信息
  23.     return this.sizeResult
  24. }
  25. //视图布局
  26. onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void {
  27.     const childHeader = children[0]
  28.     //设置header视图向上偏移自身高度
  29.     childHeader.layout({ y: -childHeader.measureResult.height })
  30.     const childContent = children[1]
  31.     //content视图不需要做偏移
  32.     childContent.layout({})
  33.     const childFooter = children[2]
  34.     //footer需要向下偏移,偏移量为内容视图高度
  35.     childFooter.layout({ y: this.sizeResult.height })
  36. }
复制代码
此时已完成初始的结构逻辑(仔细的你大概会发现一个标题,这里先挖个坑,后面细说)
接下来实现视图跟顺手势实现下拉效果
第2步实现下拉

1:content视图通过parallelGesture属性绑定PanGesture拖动手势
2:记载每次拖动手势的偏移量,header和content视图通过offset属性设置Y轴偏移量达到下拉效果
  1. //header和content视图的偏移量(视图回弹动画需要)
  2. totalOffsetY: number = 0
  3. //header和content视图的偏移量
  4. @State currentOffsetY: number = 0
  5. //记录上一次拖动手势的偏移量
  6. preOffsetY = 0;
  7. //header和content视图设置offset属性
  8. .offset({ y: this.currentOffsetY })
  9. //content视图
  10. Stack() {
  11.     this.contentView()
  12. }
  13. .offset({ y: this.currentOffsetY })
  14. .width("100%")
  15. .height("100%")
  16. .parallelGesture(PanGesture(new PanGestureOptions({ direction: PanDirection.Vertical }))
  17. .onActionStart((event: GestureEvent) => {
  18.     //Pan手势识别成功回调
  19.     //记录偏移量
  20.     this.preOffsetY = event.offsetY
  21. }).onActionUpdate((event: GestureEvent) => {
  22.     //Pan手势移动过程中回调
  23.     //新增偏移量=(当前手势偏移量-上一次手势偏移量)*0.5阻尼系数
  24.     //视图总偏移量=新增偏移量+当前视图偏移量
  25.     this.currentOffsetY = this.currentOffsetY + (event.offsetY - this.preOffsetY)*0.5
  26.     if(this.currentOffsetY<0){
  27.        //上拉视图时,不能让内容超出组件范围
  28.        this.currentOffsetY=0
  29.     }
  30.     this.totalOffsetY=this.currentOffsetY
  31.     this.preOffsetY = event.offsetY
  32.    
  33.     if(this.currentOffsetY<100){
  34.         //小于100vp时显示"下拉刷新"相关视图逻辑
  35.     }else{
  36.         //下拉距离超过100vp显示"释放刷新"相关视图逻辑
  37.     }
  38.    
  39. }).onActionEnd((event: GestureEvent)=>{
  40.     //Pan手势识别成功,手指抬起后触发回调
  41. }).onActionCancel(()=>{
  42.     //Pan手势识别成功,接收到触摸取消事件触发回调
  43.     //在窗口失焦的时候会触发
  44. }))
复制代码
此时已经实现了视图下拉的效果,接下来通过动画(animator)实现视图回弹
第3步实现视图回弹

1:给content视图设置onTouch事件监听,触发up和cancel事件时,执行回弹动画
看到这里是不是有个疑问,明明PanGesture有onActionEnd和onActionCancel事件,
在onActionEnd内里执行回弹动画不可吗?
为什么还必要监听onTouch事件?
首先触发PanGesture事件是有条件的,拖动手势必须有一小段偏移量,才能触发onActionStart事件
后续才有onActionUpdate、onActionEnd事件
也就是说触摸不一定有onActionStart事件,释放不一定有onActionEnd事件
举个例子
当视图下拉过程放手,此时视图必要回弹,在视图回弹过程中,立即触摸视图(只是按下,不拖动手势)
此时回弹动画必要制止,然后再放手(此时是不会触发onActionEnd事件),但是视图必要继承回弹
那么只能通过监听onTouch的up和cancel事件来执行回弹动画了
  1. import animator from '@ohos.animator'
  2. private animOption:AnimatorOptions={
  3.   duration: 250,
  4.   easing: "fast-out-linear-in",
  5.   delay: 0,
  6.   fill: "forwards",
  7.   direction: "normal",
  8.   iterations: 1,
  9.   begin: 0,
  10.   end: 1
  11. }
  12. private anim: AnimatorResult = animator.create(this.animOption);
  13. private animPause=false;
  14. aboutToAppear(): void {
  15.   this.anim.onFrame = (progress: number) => {
  16.     if(this.animPause){
  17.       //取消动画时,progress会变成1,如果不return this.currentOffsetY会立刻变成0
  18.       return
  19.     }
  20.     //通过总偏移量和动画执行进度,计算出当前视图偏移量
  21.     this.currentOffsetY = this.totalOffsetY * (1 - progress)
  22.   }
  23.   this.anim.onFinish=()=>{
  24.     //动画完成时,总偏移量等于当前视图偏移量
  25.     this.totalOffsetY=this.currentOffsetY
  26.   }
  27.   this.anim.onCancel=()=>{
  28.     //动画取消时,总偏移量等于当前视图偏移量
  29.     this.totalOffsetY=this.currentOffsetY
  30.   }
  31. }
  32. //给content视图设置onTouch事件
  33. .onTouch((event: TouchEvent) => {
  34.   const type = event.type
  35.   if(type==TouchType.Down){
  36.     //下拉距离大于0时,触摸content视图暂停回弹动画
  37.     if(this.currentOffsetY>0){
  38.       this.animPause=true
  39.       this.anim.cancel()
  40.     }
  41.   }else if (type == TouchType.Up || type == TouchType.Cancel) {
  42.     //下拉距离大于0时,触摸content视图暂停回弹动画
  43.     if(this.currentOffsetY>0){
  44.       this.animPause=false
  45.       //执行回弹动画
  46.       this.anim.play()
  47.     }
  48.   }
  49. })
复制代码
看到这里,仔细的你肯定又发现标题了
触摸视图时停息动画为什么要用cancel()不用pause()?
假如触摸时用pause(),不拖动手势的情况下再次释放是没什么标题的
假如触摸时通过pause()停息动画,再拖动视图(视图总偏移量发生厘革),
再次释放时就必要重新执行回弹动画,否则回弹就会出标题,
由于此时的视图总偏移量发生了厘革,继承执举措画,onFrame监听中的progress进度还是延续前次的
这样就会导致动画回弹不一连
举个例子:
开始回弹时,视图的总偏移量为100px,回弹动画进度到0.5时(onFrame监听中的progress),
触摸视图停息动画,此时视图的偏移量为:50px
然后拖动视图,使总偏移量达到100px,此时释放触发up事件时,
假如继承前次动画,那么progress大概为0.51,
onFrame监听中计算的视图偏移量就为100px*0.51=51px,
此时视图会忽然从偏移量100px变到51px,产生回弹动画不一连标题
此时已完成视图回弹至顶部的效果,
接下来实现触发刷新和修改视图回弹时保留header视图高度效果
第4步触发刷新,表现header刷新视图

下拉视图时,根据偏移量不同,表现的状态也不同
比如下拉距离<=100vp时,表现"下拉刷新"
下拉距离>=100vp时,表现"释放立即刷新"
达到刷新条件,释放手势,执行回弹动画时,保留header高度的视图,表现"正在刷新"
  1. 外部设置开始刷新的回调
  2. public onRefresh: () => void = () => {
  3. }
  4. //释放触发刷新动作
  5. .onTouch((event: TouchEvent) => {
  6.   const type = event.type
  7.   if (type == TouchType.Down) {
  8.     if (this.currentOffsetY > 0) {
  9.       this.animPause = true
  10.       this.anim.cancel()
  11.     }
  12.   } else if (type == TouchType.Up || type == TouchType.Cancel) {
  13.     this.animPause = false
  14.     if (this.currentOffsetY > 100) {
  15.       //此时修改header视图,显示正在刷新中的视图
  16.       
  17.       //如果下拉高度达到刷新条件,释放时触发刷新操作
  18.       this.onRefresh()
  19.       
  20.       /*计算回弹至header高度的progress进度*/
  21.       this.animOption.end = (this.currentOffsetY - this.headerHeight) / this.currentOffsetY
  22.       this.anim.reset(this.animOption)
  23.       this.anim.play()
  24.     } else if (this.currentOffsetY > 0) {
  25.       //执行回弹动画
  26.       this.animOption.end = 1
  27.       this.anim.reset(this.animOption)
  28.       this.anim.play()
  29.     }
  30.   }
  31. })
复制代码
此时已完成释放手势触发刷新和表现刷新中的header视图
看到这里,仔细的你又留意到了,假如外部刷新数据成功之后,如何通知组件内部并取消正在刷新中的header视图?
第5步构造controller,通知组件内部刷新完成

  1. PullToRefreshLayout({
  2.     /*设置控制器*/
  3.     controller: this.controller,
  4.     /*内容视图*/
  5.     contentView: () => {
  6.        this.contentView()
  7.     },
  8.     /*触发刷新*/
  9.     onRefresh: () => {
  10.        setTimeout(() => {
  11.            //通知刷新成功
  12.           this.controller.refreshComplete(true)
  13.       }, 1000)
  14.     }
  15.     }).width("100%").height("100%").clip(true)
  16. export class RefreshController {
  17.   /*刷新完成,true:成功,false:失败*/
  18.   refreshComplete: (isSuccess: boolean) => void = (isSuccess: boolean) => {
  19.   }
  20. }
  21. public controller: RefreshController = new RefreshController()
  22. aboutToAppear(): void {
  23.   /*通过参数通知刷新结果*/
  24.   this.controller.refreshComplete = (isSuccess: boolean) => {
  25.     if(isSuccess){
  26.         //处理刷新成功的逻辑
  27.     }else if(){
  28.         //处理刷新失败的逻辑
  29.     }
  30.     //header视图提示"刷新成功"或"刷新失败"
  31.    
  32.     //随后通过动画回弹隐藏header视图
  33.     this.animOption.end = 1
  34.     this.anim.reset(this.animOption)
  35.     this.anim.play()
  36.   }
  37. }
复制代码
第6步解决上滑再下拉,header会往下偏移标题

当内容视图是List大概其他可滑动列表时
将列表内容往上滑动,再下拉列表时,header视图会向下偏移
产生这个标题的缘故原由是下滑过程没有判定列表内容是否到顶
以是导致拖动手势对header做了向下偏移的操作
解决办法: 让外部告诉组件内部,当前列表是否到顶
  1. PullToRefreshLayout({
  2.     /*设置控制器*/
  3.     controller: this.controller,
  4.     /*内容视图*/
  5.     contentView: () => {
  6.        this.contentView()
  7.     },
  8.     /*触发刷新*/
  9.     onRefresh: () => {
  10.        setTimeout(() => {
  11.        //通知刷新成功
  12.       this.controller.refreshSuccess()
  13.       }, 1000)
  14.     },
  15.     /*是否可以下拉*/
  16.     onCanPullRefresh: () => {
  17.         if (!this.scroller.currentOffset()) {
  18.             /*处理无数据,为空的情况*/
  19.            return true
  20.         }
  21.         //如果列表到顶,返回true,表示可以下拉,返回false,表示无法下拉
  22.         return this.scroller.currentOffset().yOffset <= 0
  23.     }
  24.     }).width("100%").height("100%").clip(true)
  25. /*是否可以下拉,默认为true*/
  26. public onCanPullRefresh: () => boolean = () => true
  27. .onActionUpdate((event: GestureEvent) => {
  28.     //如果不能下拉,则header视图不发生偏移
  29.     if(!this.onCanPullRefresh()){
  30.       return
  31.     }
  32.     //Pan手势移动过程中回调
  33.     //新增偏移量=(当前手势偏移量-上一次手势偏移量)*0.5阻尼系数
  34.     //视图总偏移量=新增偏移量+当前视图偏移量
  35.     this.currentOffsetY = this.currentOffsetY + (event.offsetY - this.preOffsetY)*0.5
  36.     if(this.currentOffsetY<0){
  37.        //上拉视图时,不能让内容超出组件范围
  38.        this.currentOffsetY=0
  39.     }
  40.     this.totalOffsetY=this.currentOffsetY
  41.     this.preOffsetY = event.offsetY
  42.    
  43.     if(this.currentOffsetY<100){
  44.         //小于100vp时显示"下拉刷新"相关视图逻辑
  45.     }else{
  46.         //下拉距离超过100vp显示"释放刷新"相关视图逻辑
  47.     }
  48. })
复制代码
第7步解决下拉再上拉,列表内容会滑动标题

当内容视图是List大概其他可滑动列表时
将组件往下拉表现header视图后,再上滑时,列表内容会往上滑动
解决办法:上滑隐藏header视图时,通过Scroller设置列表滑动距离为0
  1. public scroller: Scroller | undefined = undefined
  2. .onActionUpdate((event: GestureEvent) => {
  3. if(!this.onCanPullRefresh()){
  4.   return
  5. }
  6. //存在下拉距离的情况下,向上滑动视图
  7. if (this.currentOffsetY > 0 && this.preOffsetY>event.offsetY) {
  8.   /*如果下拉再上拉,不让列表滑动*/
  9.   if (this.scroller) {
  10.     this.scroller.scrollTo({ yOffset: 0xOffset: this.scroller.currentOffset()?.xOffset ?? 0 })
  11.   }
  12. }
复制代码
  1. scroller: Scroller = new Scroller()
  2. PullToRefreshLayout({
  3.     //设置内容列表的滚动控制器
  4.     scroller: this.scroller,
  5.     controller: this.controller,
  6.     /*内容视图*/
  7.     contentView: () => {
  8.        this.contentView()
  9.     }
  10.     })
  11.     .width("100%")
  12.     .height("100%")
  13.     .clip(true)
  14.    
  15. @Builder
  16. contentView() {
  17.   List({ scroller: this.scroller }) {
  18.       
  19.   }.width("100%").height("100%")
  20.   .edgeEffect(EdgeEffect.None)  
  21. }
复制代码
仔细的你大概又发现了,为什么自定义组件用到了clip(true)属性?
这是为了解决在第1步自定义结构产生的一个标题
当header视图结构位置超出组件范围时(组件位置未超出屏幕时),是可以看见的,以是必要设置clip(true)来解决这个标题
此时已完成下拉刷新的基础逻辑,此时的你大概另有不少疑问
比如为什么只有下拉刷新,说好的上拉加载呢?
其实上拉加载的逻辑和下拉刷新雷同,一个下拉,一个上拉
能明白下拉的实现逻辑和原理,上拉加载的也就会了
最后

有许多小伙伴不知道学习哪些鸿蒙开发技能?不知道必要重点掌握哪些鸿蒙应用开发知识点?而且学习时频仍踩坑,终极浪费大量时间。以是有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习黑白常有必要的。 
点击→【纯血版鸿蒙全套最新学习资料】希望这一份鸿蒙学习资料可以大概给大家带来帮助~



 鸿蒙(HarmonyOS NEXT)最新学习门路

该门路图包罗基础技能、就业必备技能、多媒体技能、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案
门路图适合人群:
IT开发职员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技能提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技能
2.视频学习资料+学习PDF文档
获取以上完整版高清学习门路,请点击→纯血版全套鸿蒙HarmonyOS学习资料
这份鸿蒙(HarmonyOS NEXT)资料包罗了鸿蒙开发必掌握的焦点知识要点,内容包罗了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技能、Napi组件、OpenHarmony内核、(南向驱动、嵌入式等)鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技能知识点。

HarmonyOS Next 最新全套视频教程

 《鸿蒙 (OpenHarmony)开发基础到实战手册》
OpenHarmony北向、南向开发环境搭建

《鸿蒙开发基础》

《鸿蒙开发进阶》

《鸿蒙进阶实战》

大厂面试必问面试题

鸿蒙南向开发技能

鸿蒙APP开发必备

请点击→纯血版全套鸿蒙HarmonyOS学习资料




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

尚未崩坏

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表