当前位置: 首页 > news >正文

HarmonyOS7 虚拟列表不卡顿的关键在哪?动态高度和多列布局这样封装

文章目录

    • 前言
    • LazyForEach 的局限在哪
    • 动态高度:预估 + 缓存双保险
    • 多列布局:Grid 结合虚拟滚动
    • 头部吸附 Sticky Header
    • 实战:通用 VirtualList 组件
    • 下拉刷新的集成
    • 用起来的感受

前言

用过 HarmonyOS 的LazyForEach都知道,它帮我们解决了大列表全量渲染的问题。但用久了你会发现,这东西在几个场景下力不从心:列表项高度不固定、要做多列瀑布流、还要搞头部吸附——光靠原生 API 根本搞不定。

这篇文章我把这几个坑全踩一遍,最后给你一个可以直接拿去用的VirtualList组件。

LazyForEach 的局限在哪

LazyForEach的核心思路是"滑到可视区才创建组件",听着挺好。但它有个前提假设:每个列表项的高度是已知或固定的

一旦你的列表项里混着单行文本、多行图文、甚至嵌套卡片,高度参差不齐,LazyForEach就懵了——它算不准滚动偏移量,会出现跳动、白屏、定位错乱。

再加上它不支持多列布局,你想做个类似小红书的瀑布流,得自己另起炉灶。

动态高度:预估 + 缓存双保险

我的方案是"先估后测"。给每种类型的数据项一个预估高度,渲染完成后再用onAreaChange回调拿到真实高度,存进缓存。

// 高度缓存管理器classHeightCache{privatecache:Map<string,number>=newMap()privatedefaultHeight:numberconstructor(defaultHeight:number=80){this.defaultHeight=defaultHeight}getHeight(key:string):number{returnthis.cache.get(key)??this.defaultHeight}setHeight(key:string,height:number):void{this.cache.set(key,height)}// 计算指定范围内的累计高度getTotalHeight(startIndex:number,endIndex:number,keyGetter:(index:number)=>string):number{lettotal=0for(leti=startIndex;i<endIndex;i++){total+=this.getHeight(keyGetter(i))}returntotal}}

关键点在于:预估高度让滚动条一开始就有正确的比例,真实高度缓存让后续滚动越来越精准。跑通这个逻辑后,列表跳动的问题基本消失了。

多列布局:Grid 结合虚拟滚动

HarmonyOS 的WaterFlow组件本身支持虚拟滚动,但在自定义程度上有很多限制。我选择用Grid+ 手动可视区计算来实现。

思路是把数据按列数分组,每列独立维护一个高度累加器,新数据总是丢给当前最短的那列:

functiondistributeToColumns<T>(items:T[],columnCount:number,heightCache:HeightCache):T[][]{constcolumns:T[][]=Array.from({length:columnCount},()=>[])constcolumnHeights:number[]=newArray(columnCount).fill(0)for(constitemofitems){// 找最短列constminIndex=columnHeights.indexOf(Math.min(...columnHeights))columns[minIndex].push(item)columnHeights[minIndex]+=heightCache.getHeight((itemasany).id)}returncolumns}

这样做瀑布流布局,每列的高度差异最小化,视觉上更协调。

头部吸附 Sticky Header

Sticky Header 的实现核心是监听滚动偏移量。当某个 section 的 header 滚出可视区顶部时,用一个Stack在顶部叠一层"吸住"的 header。

@StatestickyHeaderIndex:number=0@StatestickyOffset:number=0// 在 onScroll 回调里计算onScroll((scrollOffset:number)=>{// 遍历 section 的累计高度,找到当前应该吸附的 sectionletaccumulated=0for(leti=0;i<this.sections.length;i++){constsectionTop=accumulatedconstsectionBottom=accumulated+this.heightCache.getHeight(`section-${i}`)if(scrollOffset>=sectionTop&&scrollOffset<sectionBottom){this.stickyHeaderIndex=i// 当下一段 header 要顶上来时,当前吸附 header 要往上推this.stickyOffset=Math.max(0,scrollOffset-sectionTop)break}accumulated=sectionBottom}})

这个stickyOffset很关键——它让吸附的 header 在被下一个 header 推走时有个自然的过渡效果,不会突然消失。

实战:通用 VirtualList 组件

把这些能力拼到一起,封装成一个通用组件。对外暴露数据源、列数、header 构建器、item 构建器:

@Componentexportstruct VirtualList<T>{@Propitems:T[]=[]@PropcolumnCount:number=1@PropestimatedItemHeight:number=80@BuilderParamitemBuilder:(item:T,index:number)=>void@BuilderParamsectionHeaderBuilder?:(sectionIndex:number)=>void@PropenableStickyHeader:boolean=false@PropenablePullRefresh:boolean=true@EventonRefresh?:()=>voidprivateheightCache:HeightCache=newHeightCache()@StateprivatestickyHeaderIndex:number=0@StateprivatestickyOffset:number=0@StateprivateisRefreshing:boolean=falsebuild(){Stack(){Scroll(){Column(){ForEach(this.sections,(section:T[],sectionIdx:number)=>{// Section Headerif(this.sectionHeaderBuilder){Column(){this.sectionHeaderBuilder(sectionIdx)}.onAreaChange((_old:Area,newArea:Area)=>{this.heightCache.setHeight(`section-${sectionIdx}`,newArea.heightasnumber)})}// 多列布局Row(){ForEach(distributeToColumns(section,this.columnCount,this.heightCache),(column:T[],colIdx:number)=>{Column(){ForEach(column,(item:T,itemIdx:number)=>{Column(){this.itemBuilder(item,itemIdx)}.onAreaChange((_old:Area,newArea:Area)=>{this.heightCache.setHeight((itemasany).id,newArea.heightasnumber)})})}.layoutWeight(1)})}})}}.onScroll((offset:number)=>{if(this.enableStickyHeader){this.updateStickyHeader(offset)}})// Sticky Header 覆盖层if(this.enableStickyHeader&&this.sectionHeaderBuilder){Column(){this.sectionHeaderBuilder(this.stickyHeaderIndex)}.translate({y:-this.stickyOffset}).position({top:0}).width('100%').zIndex(10)}}}privateupdateStickyHeader(scrollOffset:number):void{// 同上面 Sticky Header 的计算逻辑}}

下拉刷新的集成

下拉刷新直接在Scroll外面包一层Refresh组件就行,但要注意跟 Sticky Header 的层级关系:

Refresh({refreshing:this.isRefreshing}){// 上面的 Scroll 内容}.onRefreshing(()=>{this.onRefresh?.()// 数据加载完后关闭刷新状态setTimeout(()=>{this.isRefreshing=false},1000)})

用起来的感受

封装完之后,我在一个电商项目里实测了 5000 条混合高度的商品列表,滚动流畅度跟原生LazyForEach固定高度的场景几乎没有区别。多列瀑布流的列间距、item 间距都可以正常控制。

唯一需要注意的是onAreaChange在高频触发时有一定性能开销。建议在列表项类型有限的场景下,给预估高度设得准一些,减少高度缓存的修正次数。

动态高度虚拟列表这个需求,HarmonyOS 官方后续大概率会给出原生支持。但在那之前,这套方案能帮你撑过业务需求。代码量不大,但细节挺多,建议跑一遍 demo 再往项目里搬。

http://www.jsqmd.com/news/1096481/

相关文章:

  • 多通道高速采集的DDR瓶颈:你以为带宽够,其实差一个数量级
  • 面试官最爱问的流水线反压问题,我用这个Verilog握手模块搞定了
  • QY-18A、QY-18B、QY-18DL 和 QY-18DL-1 四种倾斜位移监测设备的参数对比及优劣
  • 群晖SSL证书:申请+部署+续期
  • LLM代码生成准确率已达89.7%(IEEE TSE 2024最新基准),但93%项目仍因这4个隐性缺陷失败
  • CoAP协议实战:从报文解析到工具链应用
  • 【技术解析】基于卷积神经网络的图像风格迁移:从Gatys经典算法到实践应用
  • 终极指南:3个实战场景带你玩转OpenXLSX C++ Excel库
  • 从“水泥地”到“镜面地”——地坪如何改变车间面貌
  • Flowable UI实战:从零绘制一个BPMN标准请假审批流程图
  • Flux、Mono、Reactor 核心操作符与高阶应用场景深度解析
  • 从零到一:将OpenHarmony轻量内核移植到STM32F407的实践指南
  • HarmonyOS7 全局异常怎么兜底才靠谱?错误处理和降级架构这样搭
  • 【技术解析】SimpleNet:在特征空间“制造”异常,实现高效图像缺陷检测与定位
  • LED显示屏技术图解-庖丁解牛
  • SAP-ABAP:ABAP OOP入门常见误区解析:类与对象使用的10个典型错误与避坑方案
  • 参考文献格式乱如麻?博导推荐这几个AI论文工具
  • LibreTranslate 1.9.6:三大架构突破实现边缘计算时代的离线翻译革命
  • Java while 循环
  • Python实战:基于skimage的灰度共生矩阵(GLCM)纹理特征分析与应用
  • Java Web应用安全审计实战指南:从代码到配置的全面漏洞排查
  • 基于STM32F103C8T6与HC-05的蓝牙串口透传:从零构建手机APP无线控制LED系统
  • 计算机毕业设计之大学生课堂考勤管理系统的设计与实现
  • Rocky Linux 配置 Codex + DeepSeek-V4-Pro 完整方案
  • 大模型最怕的四个字:你确定吗?
  • Cursor Free VIP破解工具:三步突破AI编程助手试用限制
  • 支持私有化部署的开源商城系统推荐:Likeshop、ShopXO深度解析
  • AI写作辅助平台8款AI论文软件梯队榜,毕业答辩稳了!
  • CAPL脚本中整型数组与Hex字符串互转的实战技巧与性能优化
  • 2026 AI营销机构选型指南:本土服务商塔米德数智科技的价值与路径