鸿蒙开发:实现一个标题栏吸顶

简介: 本身并不难,处理好滑动位置和手势即可,当然了,里面也有两个注意的点,一个是解决手势冲突的nestedScroll,这个之前的文章中讲过,还有一个就是拦截瀑布流组件的滑动事件,在某些状态下禁止它的滑动。

前言


本文基于Api13


来了一个需求,要实现顶部下拉刷新,并且顶部的标题栏,下拉状态下跟随手势刷新,上拉状态下进行吸顶,也就是tabs需要固定在顶部标题栏的下面,基本的效果可以看下图,下图是一个Demo,实际的需求,顶部标题栏带有渐变显示,不过这些不是重点。



首先要解决什么问题?第一个就是下拉刷新和上拉加载,第二个就是tabs组件进行吸顶,第三个就是手势冲突问题了,这三个问题解决了,那么效果基本上也就能实现了。


如何实现


为了保证下拉刷新是从顶部刷新,需要判断当前的滑动位置,我们可以监听Scroll组件的onReachStart事件,在这个事件里进行标记顶部的位置。


.onReachStart(() => {
        this.listPosition = RefreshPositionEnum.TOP
      })


那么同样,中间和底部的位置,我们也需要标记,中间的位置我们可以使用onScrollFrameBegin来监听,这里有一个点需要注意,因为底部是一个瀑布流组件,中间和底部的位置,完全都可以交给瀑布流组件,也就是说监听瀑布流组件的中间和底部位置。


.onReachEnd(() => {
    this.refreshPosition = RefreshPositionEnum.BOTTOM
    if (this.onRefreshPosition != undefined) {
      this.onRefreshPosition(this.refreshPosition)
    }
  })
  .onScrollFrameBegin((offset: number) => {
    if ((this.refreshPosition == RefreshPositionEnum.TOP && offset <= 0) || (
      this.refreshPosition == RefreshPositionEnum.BOTTOM && offset >= 0
    )) {
      return { offsetRemain: 0 }
    }
    this.refreshPosition = RefreshPositionEnum.CENTER //中间
    if (this.onRefreshPosition != undefined) {
      this.onRefreshPosition(this.refreshPosition)
    }
    return { offsetRemain: offset };
  })


下拉和上拉的位置确定好之后,那么就是标题栏吸顶操作了,可以看到标题栏是在底部的背景之上的,这里我们可以使用Stack组件进行包裹:


Stack() {
      Scroll() {
        Column() {
          Text("头View")
            .fontColor(Color.White)
            .width("100%")
            .height(200)
            .backgroundColor(Color.Red)
            .textAlign(TextAlign.Center)
            .margin({ top: -50 })
          Tabs({ barPosition: BarPosition.Start }) {
            TabContent() {
              this.testLayout(0)
            }.tabBar(this.tabBuilder(0, "Tab1", this))
            TabContent() {
              this.testLayout(1)
            }.tabBar(this.tabBuilder(1, "Tab2", this))
          }
          .barHeight(50)
          .vertical(false)
          .height("100%")
          .onChange((index: number) => {
            this.currentIndex = index
          })
        }.width("100%")
      }
      .padding({ top: 50 })
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .nestedScroll(this.listNestedScroll)
      //下拉刷新相关
      .onReachStart(() => {
        this.listPosition = RefreshPositionEnum.TOP
      })
      Column() {
        Text("顶部标题栏")
      }
      .width("100%")
      .height(50)
      .backgroundColor(Color.Transparent)
      .justifyContent(FlexAlign.Center)
    }.alignContent(Alignment.TopStart)


最重要的就是刷新组件了,大家可以使用自己封装的或者三方的都可以,这里我使用的是我自己封装的一个,当然了大家也可以进行使用。


地址如下:


https://ohpm.openharmony.cn/#/cn/detail/@abner%2Frefresh


源码


所有的源码如下,针对刷新库,大家如果可以切换自己的,直接替换RefreshLayout即可,当然,你可以直接使用我提供好的。


import { RefreshController, RefreshLayout, RefreshPositionEnum, WaterFlowView } from '@abner/refresh'
/**
 * AUTHOR:AbnerMing
 * DATE:2025/5/14
 * INTRODUCE:吸顶页面-瀑布流方式-固定ActionBar
 * */
@Entry
@Component
struct StickTopWaterPage {
  @State listPosition: RefreshPositionEnum = RefreshPositionEnum.BOTTOM
  @State fontColor: string = '#182431'
  @State selectedFontColor: string = '#007DFF'
  @State currentIndex: number = 0
  controller: RefreshController = new RefreshController() //刷新控制器
  @State enableScrollInteraction: boolean = true
  @State listNestedScroll?: NestedScrollOptions = {
    scrollForward: NestedScrollMode.PARENT_FIRST,
    scrollBackward: NestedScrollMode.SELF_FIRST
  }
  @Builder
  tabBuilder(index: number, name: string, _this: StickTopWaterPage) {
    Column() {
      Text(name)
        .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor)
        .fontSize(16)
        .fontWeight(this.currentIndex === index ? 500 : 400)
        .lineHeight(22)
        .margin({ top: 17, bottom: 7 })
      Divider()
        .strokeWidth(2)
        .color('#007DFF')
        .opacity(this.currentIndex === index ? 1 : 0)
    }.width('100%')
  }
  @Builder
  childView() {
    Stack() {
      Scroll() {
        Column() {
          Text("头View")
            .fontColor(Color.White)
            .width("100%")
            .height(200)
            .backgroundColor(Color.Red)
            .textAlign(TextAlign.Center)
            .margin({ top: -50 })
          Tabs({ barPosition: BarPosition.Start }) {
            TabContent() {
              this.testLayout(0)
            }.tabBar(this.tabBuilder(0, "Tab1", this))
            TabContent() {
              this.testLayout(1)
            }.tabBar(this.tabBuilder(1, "Tab2", this))
          }
          .barHeight(50)
          .vertical(false)
          .height("100%")
          .onChange((index: number) => {
            this.currentIndex = index
          })
        }.width("100%")
      }
      .padding({ top: 50 })
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .nestedScroll(this.listNestedScroll)
      //下拉刷新相关
      .onReachStart(() => {
        this.listPosition = RefreshPositionEnum.TOP
      })
      Column() {
        Text("顶部标题栏")
      }
      .width("100%")
      .height(50)
      .backgroundColor(Color.Transparent)
      .justifyContent(FlexAlign.Center)
    }.alignContent(Alignment.TopStart)
  }
  build() {
    Column() {
      RefreshLayout({
        itemLayout: () => {
          this.childView()
        },
        controller: this.controller,
        refreshPosition: this.listPosition, //定位位置
        isRefreshTopSticky: true, //是否顶部吸顶
        isRefreshTopTitleSticky: true,
        enableScrollInteraction: (interaction: boolean) => {
          this.enableScrollInteraction = interaction
        },
        onStickyNestedScroll: (nestedScroll: NestedScrollOptions) => {
          this.listNestedScroll = nestedScroll
        },
        onRefresh: () => {
          setTimeout(() => {
            //模拟耗时
            this.controller.finishRefresh()
          }, 3000)
        },
        onLoadMore: () => {
          setTimeout(() => {
            //模拟耗时
            this.controller.finishLoadMore()
          }, 3000)
        }
      })
    }
  }
  /*
  * Author:AbnerMing
  * Describe:这里仅仅是测试,实际应以业务需求为主,可以是任意得组件视图
  */
  @Builder
  testLayout(type: number) {
    StickyStaggeredView({
      pageType: type,
      nestedScroll: this.listNestedScroll,
      enableScrollInteraction: this.enableScrollInteraction,
      onRefreshPosition: (refreshPosition: RefreshPositionEnum) => {
        if (refreshPosition != RefreshPositionEnum.TOP) {
          this.listPosition = refreshPosition
        }
      }
    })
  }
}
/*
* Author:AbnerMing
* Describe:瀑布流页面
*/
@Component
struct StickyStaggeredView {
  @State pageType: number = 0
  controller: RefreshController = new RefreshController() //刷新控制器
  @State arr1: number[] = [] //实际情况当以tab指示器对应得数据为主,这里仅仅是测试
  @State arr2: number[] = []
  private itemHeightArray: number[] = []
  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]
  @State minSize: number = 80
  @State maxSize: number = 180
  @Prop nestedScroll: NestedScrollOptions = {
    scrollForward: NestedScrollMode.SELF_FIRST,
    scrollBackward: NestedScrollMode.PARENT_FIRST
  }
  onRefreshPosition?: (refreshPosition: RefreshPositionEnum) => void //回调位置
  @Prop enableScrollInteraction: boolean = true; //拦截列表
  // 计算FlowItem宽/高
  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize)
    return (ret > this.minSize ? ret : this.minSize)
  }
  // 设置FlowItem的宽/高数组
  setItemSizeArray() {
    for (let i = 0; i < 100; i++) {
      this.itemHeightArray.push(this.getSize())
    }
  }
  aboutToAppear() {
    for (let i = 0; i < 30; i++) {
      this.arr1.push(i)
    }
    for (let i = 0; i < 50; i++) {
      this.arr2.push(i)
    }
    this.setItemSizeArray()
  }
  @Builder
  itemLayout(_this: StickyStaggeredView, _: Object, index: number) {
    Column() {
      Text("测试数据" + index)
    }.width("100%")
    .height(this.itemHeightArray[index % 100])
    .backgroundColor(this.colors[index % 5])
  }
  build() {
    WaterFlowView({
      items: this.pageType == 0 ? this.arr1 : this.arr2,
      itemView: (item: Object, index: number) => {
        this.itemLayout(this, item, index)
      },
      nestedScroll: this.nestedScroll,
      onRefreshPosition: this.onRefreshPosition,
      enableScrollInteraction: this.enableScrollInteraction,
    })
  }
}


相关总结


本身并不难,处理好滑动位置和手势即可,当然了,里面也有两个注意的点,一个是解决手势冲突的nestedScroll,这个之前的文章中讲过,还有一个就是拦截瀑布流组件的滑动事件,在某些状态下禁止它的滑动。


本文标签:HarmonyOS/ArkUI

相关文章
|
26天前
|
开发者
鸿蒙开发:资讯项目实战之项目初始化搭建
目前来说,我们的资讯项目只是往前迈了很小的一步,仅仅实现了项目创建,步虽小,但概念性的知识很多,这也是这个项目的初衷,让大家不仅仅可以掌握日常的技术开发,也能让大家理解实际的项目开发知识。
鸿蒙开发:资讯项目实战之项目初始化搭建
|
20天前
|
缓存 JavaScript IDE
鸿蒙开发:基于最新API,如何实现组件化运行
手动只是让大家了解切换的原理,在实际开发中,可不推荐手动,下篇文章,我们将通过脚本或者插件,快速实现组件化模块之间的切换,实现独立运行,敬请期待!
鸿蒙开发:基于最新API,如何实现组件化运行
|
26天前
|
SQL 弹性计算 数据库
鸿蒙5开发宝藏案例分享---优化应用时延问题
鸿蒙性能优化指南来了!从UI渲染到数据库操作,6大实战案例助你提升应用流畅度。布局层级优化、数据加载并发、数据库查询提速、相机资源延迟释放、手势识别灵敏调整及转场动画精调,全面覆盖性能痛点。附赠性能自检清单,帮助开发者高效定位问题,让应用运行如飞!来自华为官方文档的精华内容,建议收藏并反复研读,共同探讨更多优化技巧。
|
26天前
|
缓存
鸿蒙5开发宝藏案例分享---Swiper组件性能优化实战
本文分享了鸿蒙系统中Swiper组件的性能优化技巧,包括:1) 使用`LazyForEach`替代`ForEach`实现懒加载,显著降低内存占用;2) 通过`cachedCount`精准控制缓存数量,平衡流畅度与内存消耗;3) 利用`onAnimationStart`在抛滑时提前加载资源,提升构建效率;4) 添加`@Reusable`装饰器复用组件实例,减少创建开销。实际应用后,图库页帧率从45fps提升至58fps,效果显著。适合处理复杂列表或轮播场景,欢迎交流经验!
|
26天前
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
26天前
|
数据库
鸿蒙5开发宝藏案例分享---跨线程性能优化指南
本文深入探讨鸿蒙系统跨线程序列化性能优化,借助DevEco Profiler工具定位序列化瓶颈。通过Sendable接口改造、数据瘦身等方法,将5万本书对象的序列化耗时从260ms+降至&lt;8ms,甚至&lt;1ms。总结避坑经验,建议常态化使用Profiler检测,避免传递大对象,提升多线程开发效率。
|
缓存 数据管理 Shell
鸿蒙5开发宝藏案例分享---性能分析简介
鸿蒙开发资源大揭秘!文中整理了HarmonyOS官方提供的100+场景化案例,涵盖性能优化、UI设计、设备适配等全链路内容。重点解析三大神级案例:折叠屏悬停交互、万人列表流畅滚动和服务卡片实时刷新,附带完整代码与避坑指南。通过精准搜索、代码移植和调试技巧,高效利用这些宝藏资源,助你省时省力避开开发陷阱。更有抖音级短视频流畅度优化方案等彩蛋等待探索!
|
26天前
|
Java
鸿蒙5开发宝藏案例分享---性能检测工具揭秘
鸿蒙性能优化工具全揭秘!本文详解官方隐藏的性能调优利器,包括静态检测(Code Linter)与动态检测(AppAnalyzer)。通过实战案例解析稀疏数组陷阱、循环更新状态变量等问题,并提供优化方案。同时附带高频性能规则速查表及黄金法则,助你高效避坑。开发时建议双开工具,实时检测问题,提升应用性能。
|
缓存 Shell 开发者
鸿蒙5开发宝藏案例分享---性能体验设计
这是一篇关于HarmonyOS性能优化的开发者实践指南。文章结合官方文档案例与代码实现,分享了流畅性设计的实用技巧,包括感知流畅性的核心原则、交互流畅实战案例(如列表滑动优化和点击响应加速)、视觉流畅的动效设计,以及性能检测工具ArkUI Inspector的使用方法。最后还提供了冷启动优化的具体策略。通过这些内容,帮助开发者打造60帧无卡顿的鸿蒙应用,实现操作响应快速、动效流畅的目标。
|
26天前
|
存储 C++ UED
鸿蒙5开发宝藏案例分享---优化应用包体积大小问题
本文分享了鸿蒙应用包体积优化的实用技巧,包括SO库压缩、HSP动态共享包、OHPM依赖冲突解决、按需加载和扫描工具定位优化点等方法。通过具体配置示例和实战经验,如启用`compressNativeLibs`、使用共享资源包、强制统一依赖版本以及动态导入功能模块,帮助开发者显著减少包体积,提升用户体验。文中还提供了图标优化、资源混淆和无用代码剔除等补充建议,助力打造更轻量的鸿蒙应用。