Harmory Next 下拉刷新 悬浮loading效果

因为业务需要在下拉时列表不能滑动,而是一个悬浮式的loading跟手滑动,在全网找了一圈都没找到案例,只能自己动手写一个demo去试验,发现这东西还真有点难... 不说了 直接上效果吧 问题肯定是有的 但核心逻辑应该能行得通

效果图(模拟器环境下)

核心代码

ArkTS 复制代码
@Component
export struct SuspensionRefresh {
  // scroll controller
  scrollController:Scroller = new Scroller()

  // default slot
  @BuilderParam content:() => void = this.defaultContent

  // 刷新控制变量 必传
  @Link @Watch('progressAnimation') isRefresh:boolean
  // 设置下拉距离多少时可触发刷新
  @Prop refreshOffset:number = 64
  // 设置下拉距离的极限
  @Prop refreshOffsetLimit:number = 96


  // 下滑距离
  @State pullOffsetY:number = -30
  // 识别距离 即下拉多少时认为开始触发下拉动画
  @State pullRefreshStartLimit:number = 10
  // 记录每一次触发滑动事件时 上一次和这一次的滑动距离
  @State currPullOffsetY:number = 0
  @State lastPullOffsetY:number = 0
  // 透明度
  @State opacityValue:number = 0
  // 进度条值
  @State progressValue:number = 0
  // 定时器
  @State timer:number = -1
  // 比值
  @State radio:number = 0
  // 是否正在下拉中
  @State isPulling:boolean = false
  // 内部内容是否触顶
  @State isReactPageByTop:boolean = true

  // 动画计算参数 start
  // 旋转角度
  @State rotateAngle:number = 0
  // 旋转角度最大值
  @State rotateAngleLimit:number = 360
  // 动画计算参数 end

  // callback start
  onRefreshHandler: () => void = async () => {}
  // callback end

  progressAnimation(){

  }

  build() {
    this.layout()
  }

  /*
   * 布局builder
   * */
  @Builder
  layout(){
    Stack(){
      Column(){
        Scroll(this.scrollController){
          this.content()
        }
        .layoutContainerStyleByScroll()
        .enableScrollInteraction(!this.isPulling)
        .onReachStart(() => {
          this.isReactPageByTop = true
        })
        .onDidScroll(() => {
          if (this.scrollController.currentOffset().yOffset !== 0) {
            this.isReactPageByTop = false
          }
        })
      }
      .full()
      // 并行手势
      .parallelGesture(
        // 滑动手势
        PanGesture({fingers:1,distance:this.pullRefreshStartLimit,direction:PanDirection.Vertical})
          // 手势开始时
          .onActionStart(() => {
            this.currPullOffsetY = 0
            this.lastPullOffsetY = 0
          })
          // 手势中
          .onActionUpdate((event) => {
            // 记录当前滑动位置 以及记录上一次的滑动位置
            this.lastPullOffsetY = this.currPullOffsetY
            this.currPullOffsetY = event.offsetY
            // 计算比值
            let radio = this.pullOffsetY / this.refreshOffsetLimit
            // 判断下拉还是上拉
            let isPullDown:boolean = this.currPullOffsetY - this.lastPullOffsetY > 0 ? true : false

            // 如果此时触顶并下拉 则标记为下拉状态中
            if (this.isReactPageByTop && isPullDown){
              this.isPulling = true
            }

            // 当前不在刷新中
            if (!this.isRefresh) {
              // 下拉时
              if (isPullDown){
                // 如果是下拉状态中 再计算位置
                if (this.isPulling) {
                  // 此时如果超出下拉最大距离 则给一个固定值
                  if (this.pullOffsetY >= this.refreshOffsetLimit) {
                    this.pullOffsetY = this.refreshOffsetLimit
                    this.rotateAngle = this.rotateAngleLimit
                  }
                  // 如果没超出 则计算比值并按比例赋值
                  else {
                    this.pullOffsetY += (this.currPullOffsetY - this.lastPullOffsetY)
                    this.rotateAngle = radio * this.rotateAngleLimit
                    // 如果已经达到刷新距离但未到极限距离 则把透明度给一个默认值
                    if (this.pullOffsetY > this.refreshOffset) {
                      this.opacityValue = 1
                    }
                    else {
                      this.opacityValue = (this.pullOffsetY / this.refreshOffset)
                    }
                  }
                }
              }
              // 上拉时
              else {
                // 如果此时仍在下拉中 则继续改变loading位置
                if (this.isPulling) {
                  this.pullOffsetY += (this.currPullOffsetY - this.lastPullOffsetY)
                  this.rotateAngle = radio * this.rotateAngleLimit
                  // 如果已经达到刷新距离但未到极限距离 则把透明度给一个默认值
                  if (this.pullOffsetY > this.refreshOffset) {
                    this.opacityValue = 1
                  }
                  else {
                    this.opacityValue = (this.pullOffsetY / this.refreshOffset)
                  }
                }
              }
            }

          })
          // 手势结束时
          .onActionEnd(async (event) => {
            // 如果下拉距离已经超出 就赋值固定值
            let timer = -1
            let isAdd = true
            if (!this.isRefresh){
              if (this.pullOffsetY > this.refreshOffset) {
                this.isRefresh = true

                this.pullOffsetY = this.refreshOffsetLimit
                this.rotateAngle = this.rotateAngleLimit

                timer = setInterval(()=>{
                  if (this.progressValue === 100) {
                    isAdd = false
                  }
                  else if (this.progressValue <= 0) {
                    isAdd = true
                  }

                  if (isAdd) {
                    this.progressValue += 2
                  }
                  else {
                    this.progressValue -= 2
                  }
                  this.rotateAngle += 5
                },10)

                // 执行刷新方法
                await this.onRefreshHandler()
              }
              // 清空定时器
              clearInterval(timer)

              // 恢复初始状态
              this.isRefresh = false
              this.isPulling = false
              this.pullOffsetY = -30
              this.opacityValue = 0
              this.isReactPageByTop = true
            }
          })
          // 取消手势
          .onActionCancel(() => {
            console.log('cancel')
          })
      )

      this.refreshLoadingBuilder()
    }
    .full()
    .align(Alignment.Top)
  }

  /*
   * 占位builder
   * */
  @Builder
  defaultContent(){
    Column(){
      // 可以放置通用空状态
    }
    .full()
  }

  /*
   * 刷新loading
   * */
  @Builder
  refreshLoadingBuilder(){
    Column(){
      if (this.isRefresh){
        Progress({type:ProgressType.Ring,value:this.progressValue,total:100})
          .width(20)
          .height(20)
          .color('#FF4C58')
          .style({
            strokeWidth:2,
          })
          .backgroundColor(Color.Transparent)
      }
      else {
        SymbolGlyph($r('sys.symbol.arrow_clockwise'))
          .fontSize(20)
          .fontWeight(700)
          .fontColor(['#FF4C58'])
      }
    }
    .translate({
      y:this.pullOffsetY
    })
    .rotate({
      x:0,
      y:0,
      z:1,
      angle:this.rotateAngle
    })
    .opacity(this.opacityValue)
    .animation({
      duration:100,
      curve:Curve.Linear
    })
    .refreshLoadingContainerStyle()
  }
}
// 样式-占满
@Styles
function full(){
  .width('100%')
  .height('100%')
}

@Styles
function refreshLoadingContainerStyle(){
  .borderRadius(20)
  .backgroundColor('#FFFFFF')
  .zIndex(100)
  .padding(5)
}

@Extend(Scroll)
function layoutContainerStyleByScroll(){
  .width('100%')
  .height('100%')
  .align(Alignment.Top)
  .scrollBar(BarState.Off)
}
相关推荐
不凡的凡5 小时前
鸿蒙图片相似性对比
华为·harmonyos
Georgewu7 小时前
【HarmonyOS】HAR和HSP循环依赖和依赖传递问题详解
harmonyos
Georgewu9 天前
【HarmonyOS 5】鸿蒙跨平台开发方案详解(一)
flutter·harmonyos
万少10 天前
重磅推出 🔥 HarmonyOS AI 助手 CodeGenie V6 的使用教程
前端·harmonyos
我睡醒再说10 天前
纯血HarmonyOS5 打造小游戏实践:绘画板(附源文件)
harmonyos
我睡醒再说10 天前
HarmonyOS 5 ArkTS Worker线程:构建高性能移动应用的并行计算引擎
harmonyos
我睡醒再说10 天前
纯血HarmonyOS5 打造小游戏实践:扫雷(附源文件)
harmonyos
二流小码农10 天前
鸿蒙开发:基于node脚本实现组件化运行
android·ios·harmonyos
中雨202510 天前
HarmonyOS Next快速入门:类型定义和国际化
harmonyos