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)
}
相关推荐
zhanshuo11 小时前
构建可扩展的状态系统:基于 ArkTS 的模块化状态管理设计与实现
harmonyos
zhanshuo11 小时前
ArkTS 模块通信全解析:用事件总线实现页面消息联动
harmonyos
codefish79816 小时前
鸿蒙开发学习之路:从入门到实践的全面指南
harmonyos
yrjw1 天前
一款基于react-native harmonyOS 封装的【文档】文件预览查看开源库(基于Harmony 原生文件预览服务进行封装)
harmonyos
搜狐技术产品小编20232 天前
搜狐新闻直播间适配HarmonyOs实现点赞动画
华为·harmonyos
zhanshuo2 天前
ArkUI 玩转水平滑动视图:超全实战教程与项目应用解析
harmonyos·arkui
zhanshuo2 天前
ArkUI Canvas 实战:快速绘制柱状图图表组件
harmonyos·arkui
zhanshuo2 天前
手把手教你用 ArkUI 写出高性能分页列表:List + onScroll 实战解析
harmonyos
zhanshuo2 天前
深入解析 ArkUI 触摸事件机制:从点击到滑动的开发全流程
harmonyos
i仙银3 天前
鸿蒙沙箱浏览器 - SandboxFinder
app·harmonyos