HarmonyOS 6.0 列表分组与吸顶索引
通讯录按字母分组、商品按类别分组、订单按日期分组——列表分组是高频需求。但分组不只是给数据排序加标题还有吸顶效果、侧边索引条、分组折叠、跨分组全选。每个功能单独做不难组合起来坑就来了。数据分组第一步是把扁平数据按字段分组。最常见的是按首字母分组interfaceContact{id:number;name:string;phone:string;pinyin:string;}interfaceGroupedContact{letter:string;contacts:Contact[];}privategroupByLetter(contacts:Contact[]):GroupedContact[]{letgroups:GroupedContact[][];letgroupMap:Recordstring,Contact[]{};for(leti0;icontacts.length;i){letfirstCharcontacts[i].pinyin.charAt(0).toUpperCase();if(groupMap[firstChar]undefined){groupMap[firstChar][];}groupMap[firstChar].push(contacts[i]);}letlettersObject.keys(groupMap).sort();for(leti0;iletters.length;i){groups.push({letter:letters[i],contacts:groupMap[letters[i]]});}returngroups;}先建一个首字母→联系人的映射再按字母排序输出分组。Object.keys返回的顺序不保证必须sort。按日期分组同理interfaceGroupedOrder{date:string;orders:Order[];}privategroupByDate(orders:Order[]):GroupedOrder[]{letgroups:GroupedOrder[][];letgroupMap:Recordstring,Order[]{};for(leti0;iorders.length;i){letdateStrthis.formatDate(orders[i].createTime);if(groupMap[dateStr]undefined){groupMap[dateStr][];}groupMap[dateStr].push(orders[i]);}letdatesObject.keys(groupMap).sort();dates.reverse();for(leti0;idates.length;i){groups.push({date:dates[i],orders:groupMap[dates[i]]});}returngroups;}日期分组通常是倒序——最新的在前。reverse()翻转排序结果。分组列表渲染用ListItemGroup实现分组渲染List(){ForEach(this.groupedContacts,(group:GroupedContact){ListItemGroup({header:this.groupHeader(group.letter),divider:{strokeWidth:1,color:#f0f0f0}}){ForEach(group.contacts,(contact:Contact){ListItem(){Row(){Text(contact.name).fontSize(16).fontColor(#333333)Text(contact.phone).fontSize(14).fontColor(#999999).margin({left:12})}.width(100%).padding({left:16,right:16,top:12,bottom:12})}},(contact:Contact)contact.id.toString())}},(group:GroupedContact)group.letter)}BuildergroupHeader(letter:string){Row(){Text(letter).fontSize(14).fontWeight(FontWeight.Bold).fontColor(#666666)}.width(100%).padding({left:16,top:8,bottom:8}).backgroundColor(#f5f5f5)}ListItemGroup自带header和divider参数省去手动添加分组标题的麻烦。header接受Builder可以自定义样式。吸顶效果ListItemGroup的header默认随列表滚动而消失。要实现吸顶sticky header设sticky属性ListItemGroup({header:this.groupHeader(group.letter),divider:{strokeWidth:1,color:#f0f0f0}}){// 列表项}.sticky(StickyStyle.Header)StickyStyle.Header让header在滚动到顶部时粘住直到被下一个header推走。这是通讯录字母吸顶的标准实现。吸顶的header会覆盖在列表内容上方所以header要有背景色。透明的header会透出下面的内容看起来很怪。侧边字母索引通讯录右侧的A-Z快速跳转Stack(){List(){ForEach(this.groupedContacts,(group:GroupedContact){ListItemGroup({header:this.groupHeader(group.letter)}){ForEach(group.contacts,(contact:Contact){ListItem(){// 内容}},(contact:Contact)contact.id.toString())}.sticky(StickyStyle.Header)},(group:GroupedContact)group.letter)}.width(100%).height(100%)// 侧边索引Column(){ForEach(this.alphabet,(letter:string){Text(letter).fontSize(10).fontColor(this.activeLetterletter?#1a73e8:#666666).fontWeight(this.activeLetterletter?FontWeight.Bold:FontWeight.Normal).padding(2).onClick((){this.scrollToLetter(letter);})},(letter:string)letter)}.position({x:92%,y:60}).height(70%).justifyContent(FlexAlign.SpaceBetween)}alphabet是A-Z的字符串数组。点击字母时调用scrollToLetter滚动到对应分组。List滚动到指定位置用Scrollerprivatescroller:ScrollernewScroller();privateletterToIndex:Recordstring,number{};// 初始化映射aboutToAppear():void{for(leti0;ithis.groupedContacts.length;i){this.letterToIndex[this.groupedContacts[i].letter]i;}}// 滚动到指定字母privatescrollToLetter(letter:string):void{letindexthis.letterToIndex[letter];if(index!undefined){this.scroller.scrollToIndex(index);this.activeLetterletter;}}scroller.scrollToIndex跳转到指定ListItemGroup的位置。letterToIndex是字母→分组索引的映射表在数据加载后构建。注意scrollToIndex的索引对应的是ListItemGroup在List中的位置不是ListItem。如果List中只有ListItemGroup没有单独的ListItem索引就是ListItemGroup的序号。滑动联动手指滑动列表时右侧索引高亮要跟着变List(){// 内容}.onScrollIndex((start:number,end:number){if(start0startthis.groupedContacts.length){this.activeLetterthis.groupedContacts[start].letter;}})onScrollIndex在滚动时回调当前可见区域的起始和结束索引。start就是当前吸顶的分组索引用它更新activeLetter。索引和高亮的联动是双向的点击索引滚动列表滑动列表更新索引。两边都要处理。手指滑动索引更流畅的体验是手指在索引条上滑动时实时跳转Column(){ForEach(this.alphabet,(letter:string){Text(letter).fontSize(10).fontColor(this.activeLetterletter?#1a73e8:#666666).padding(2)},(letter:string)letter)}.position({x:92%,y:60}).height(70%).justifyContent(FlexAlign.SpaceBetween).gesture(PanGesture().onActionStart((event:GestureEvent){this.handleIndexTouch(event.y);}).onActionUpdate((event:GestureEvent){this.handleIndexTouch(event.yevent.offsetY);}))privatehandleIndexTouch(y:number):void{letindexMath.floor(y/(this.indexHeight));if(index0indexthis.alphabet.length){letletterthis.alphabet[index];this.scrollToLetter(letter);}}PanGesture滑动时根据y坐标计算手指在哪个字母上实时跳转。indexHeight是每个字母的占用高度需要根据索引条总高度和字母数量计算。分组折叠长列表分组可折叠减少滚动距离StatecollapsedGroups:SetstringnewSet();BuildergroupHeader(letter:string,count:number){Row(){Text(letter).fontSize(14).fontWeight(FontWeight.Bold).fontColor(#666666)Text(count.toString()人).fontSize(12).fontColor(#999999).margin({left:8})Blank()Text(this.collapsedGroups.has(letter)?展开:收起).fontSize(12).fontColor(#1a73e8)}.width(100%).padding({left:16,right:16,top:10,bottom:10}).backgroundColor(#f5f5f5).onClick((){if(this.collapsedGroups.has(letter)){this.collapsedGroups.delete(letter);}else{this.collapsedGroups.add(letter);}this.collapsedGroupsnewSet(this.collapsedGroups);})}Set存储折叠的分组key。点击header时切换折叠状态。注意Set的delete不会触发UI刷新需要重新赋值一个新的Set。折叠时只渲染header不渲染childrenListItemGroup({header:this.groupHeader(group.letter,group.contacts.length)}){if(!this.collapsedGroups.has(group.letter)){ForEach(group.contacts,(contact:Contact){ListItem(){// 内容}},(contact:Contact)contact.id.toString())}}分组全选勾选整个分组的所有项StateselectedIds:SetnumbernewSet();privatetoggleGroup(letter:string,contacts:Contact[]):void{letallSelectedtrue;for(leti0;icontacts.length;i){if(!this.selectedIds.has(contacts[i].id)){allSelectedfalse;break;}}if(allSelected){for(leti0;icontacts.length;i){this.selectedIds.delete(contacts[i].id);}}else{for(leti0;icontacts.length;i){this.selectedIds.add(contacts[i].id);}}this.selectedIdsnewSet(this.selectedIds);}先检查分组内是否全选——全选则取消全选否则全选。toggleGroup的调用放在header的onClick中跟折叠互斥时用checkbox代替点击。搜索过滤与分组联动搜索过滤后分组可能变空要清理空分组privatefilterAndGroup(keyword:string):GroupedContact[]{letfiltered:Contact[][];for(leti0;ithis.allContacts.length;i){if(this.allContacts[i].name.indexOf(keyword)0||this.allContacts[i].phone.indexOf(keyword)0){filtered.push(this.allContacts[i]);}}letgroupedthis.groupByLetter(filtered);returngrouped;}先过滤再分组。如果某个字母下没有匹配的联系人groupByLetter自然不会生成该分组不需要额外清理。搜索时侧边索引也要更新——只显示有数据的字母privategetavailableLetters():string[]{letletters:string[][];for(leti0;ithis.groupedContacts.length;i){letters.push(this.groupedContacts[i].letter);}returnletters;}踩坑清单问题原因解决吸顶header透字header背景透明给header加backgroundColorscrollToIndex跳错位置索引对应的是ListItem不是ListItemGroup用ListItemGroup的序号滑动时索引不高亮没处理onScrollIndex回调中更新activeLetterSet操作不刷新UIdelete/add不触发检测重新赋值new Set(oldSet)搜索后索引没更新索引字母是固定的从分组数据动态计算分组数据为空过滤后某分组无匹配项过滤后重新分组ListItemGroup的key重复用index作key用group.letter作key索引条位置偏移position用了百分比跟容器宽度协调计算PanGesture和List冲突索引条手势被List消费索引条用priorityGesture折叠动画闪烁数据变化和动画不同步折叠直接切换不做动画分组列表的核心是数据分组→ListItemGroup渲染→Scroller跳转→索引联动这条链路。吸顶用StickyStyle.Header索引用ScrolleronScrollIndex两者联动就构成了通讯录的完整体验。

相关新闻

Jellium Desktop窗口尺寸记忆设置:为不同内容类型保存大小

Jellium Desktop窗口尺寸记忆设置:为不同内容类型保存大小

Jellium Desktop窗口尺寸记忆设置:为不同内容类型保存大小 【免费下载链接】jellium-desktop An unofficial desktop client for Jellyfin 项目地址: https://gitcode.com/GitHub_Trending/je/jellium-desktop Jellium Desktop作为一款非官方的Jellyfin桌面客…

2026/10/12 17:20:28 阅读更多 →
HarmonyOS 6.0 页面间数据传递与参数序列化

HarmonyOS 6.0 页面间数据传递与参数序列化

页面跳转带数据,这谁不会?pushPath的时候把参数传过去就完了。但现实远没这么简单——传个字符串没问题,传个对象呢?传个Date呢?传个100KB的JSON呢?跨Ability跳转呢?返回时怎么带数据回来&#…

2026/10/13 16:41:50 阅读更多 →
锐龙AI嵌入式X100与Kria AI齐发,AMD打通物理AI“任督二脉”

锐龙AI嵌入式X100与Kria AI齐发,AMD打通物理AI“任督二脉”

作者:毛烁 “物理 AI(Physical AI)是科技领域最艰巨的挑战之一。而实现这一级别的智能,需要一套全栈式解决方案。”在今年的CES大会上,AMD 董事长兼CEO 苏姿丰(Lisa Su)博士的这番判断&#xff…

2026/10/6 4:57:38 阅读更多 →

最新新闻

PS5串流实战指南:从局域网到外网的远程游戏配置与延迟优化

PS5串流实战指南:从局域网到外网的远程游戏配置与延迟优化

直接接PS5玩家一个很现实的问题:这台机器买回来之后,是不是基本就长在客厅电视柜上了?客厅被占、卧室想玩、出差想刷两把,每次都只能乖乖回到电视前。我捣鼓AnyPS5这套串流方案,就是为了把这台主机从HDMI线的长度里解放…

2026/10/13 20:09:06 阅读更多 →
若依vue3前端页面框架:路由联动、权限指令与缓存机制解析

若依vue3前端页面框架:路由联动、权限指令与缓存机制解析

简介:若依vue3前端页面框架是一套基于Vue3与Element Plus的开源后台管理前端解决方案,面向需要快速搭建企业级系统的开发团队及初中级前端工程师,适用于中后台管理系统、权限控制界面、数据看板等典型场景。压缩包共299个文件、约3MB&#xf…

2026/10/13 20:09:06 阅读更多 →
嵌入式视觉系统中MT9M001C到LCD的端到端时序设计

嵌入式视觉系统中MT9M001C到LCD的端到端时序设计

简介:本资源是面向FPGA初学者与嵌入式图像处理学习者的实战项目,聚焦于MT9M001 CMOS图像传感器的驱动与实时显示实现,适用于基于Altera/Intel FPGA平台的开发板验证与课程实验。项目完整呈现了从传感器数据采集、FPGA逻辑控制(含时…

2026/10/13 20:09:06 阅读更多 →
PS5全机型优化指南:从版本识别、M.2扩展到HDMI 2.1设置与故障排查

PS5全机型优化指南:从版本识别、M.2扩展到HDMI 2.1设置与故障排查

如果你手头有一台 PS5,或者正打算入手,不管是初版、轻薄版还是 Pro 版,这篇文章都是给你准备的。我写这个东西的起因很简单:群里隔三差五就有人问"我这个 PS5 为什么风扇声音这么大""到底要不要上 Pro""…

2026/10/13 20:09:06 阅读更多 →
STM32跑马灯实验详解:从时钟配置到GPIO驱动与工程下载

STM32跑马灯实验详解:从时钟配置到GPIO驱动与工程下载

简介:这份资源是面向嵌入式初学者与STM32入门者的跑马灯实验例程包,围绕GPIO输出控制这一核心知识点,帮助读者快速上手STM32微控制器的基本开发流程。资源以ALIENTEK MINISTM32开发板为硬件平台,配套完整工程代码与详细注释&#…

2026/10/13 20:09:06 阅读更多 →
Steinberg VST插件开发包入门:从零构建VST3音频插件

Steinberg VST插件开发包入门:从零构建VST3音频插件

简介:Steinberg VST插件开发包(VST3 SDK 3.5版)面向音频插件开发者、音乐软件工程师及数字音频工作站扩展爱好者,提供创建虚拟乐器与音频效果处理器所需的完整开发环境。包内共5764个文件,以html文档、png界面素材、h头…

2026/10/13 20:08:06 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →