源码获取

如果你想一边对照文章一边实操,建议直接把示例工程拉到本地。项目 Git 地址:https://gitcode.com/HarmonyOS_Samples/CommonListFlows

A hand-drawn sketch-notes style infographic explai

第一次看到“分组标题吸顶”的页面,很多人会本能地觉得这玩意儿很高级。

然后脑子里立刻开始脑补: 要不要监听滚动距离,要不要自己算偏移,要不要动态改定位。结果还没写代码,人已经先把自己吓着了。

说实话,这种担心大多是想多了。

在 ArkUI 的 List 体系里,分组吸顶真正关键的不是你会不会手搓动画,而是你有没有把列表结构组织对。这个项目里的 HomePageCityList,刚好把这件事讲得很明白。

吸顶这件事,难点不在动画,而在分组

先把概念掰直。

所谓分组吸顶,就是你往下滑的时候,当前分组标题固定在顶部,等下一个分组上来,再把它顶走。

这种交互非常常见:

  • 城市选择页
  • 通讯录
  • 分类商品列表
  • 按日期分组的消息页

所以你现在学的不是一个花哨技巧,而是一类特别高频的页面基础能力。

真正的前提不是 sticky,而是 ListItemGroup

先看 HomePage 里景区分组的写法:

ForEach(this.scenicSpotTitle, (item: Resource) => {
  ListItemGroup({ header: this.scenicSpotHeader(item) }) {
    ForEach(this.scenicSpotArray, (scenicSpotItem: Resource) => {
      ListItem() {
        this.scenicSpotDetailBuilder(scenicSpotItem)
      }
    }, (scenicSpotItem: Resource) => JSON.stringify(scenicSpotItem))
  }
}, (item: Resource) => JSON.stringify(item))

这段代码最值得记住的不是遍历细节,而是结构关系:

  • 外层按分类分组
  • 每一组都用 ListItemGroup
  • 组里面再放具体 ListItem

这一步才是吸顶真正的地基。

因为框架只有先知道“这是一组内容,这是一组的头部”,后面才有可能帮你做吸顶。你如果连分组关系都没建立,光盯着吸顶效果本身,基本是在白忙活。

A technical blueprint-style diagram comparing two

标题头为什么最好单独抽出来

项目把分组头专门写成了一个 Builder:

@Builder
scenicSpotHeader(title: Resource) {
  Column() {
    Text(title)
      .width('100%')
      .height(50)
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .backgroundColor(0xF1F3F5)
  }
}

这一步很实用。

因为分组标题通常不是只出现一次。你今天想改高度,明天想改背景色,后天又想加图标或者间距,如果一开始就散落在各处,后面会改得很烦。

抽出来以后,结构更清楚,维护也轻松。

真正让标题吸住顶部的,就这一行

很多人以为要写很长的滚动逻辑,其实项目里真正生效的是这句:

.sticky(StickyStyle.Header)

它直接挂在 List 上:

List({ space: 12 }) {
  // 列表内容
}
.sticky(StickyStyle.Header)

说白了,ArkUI 已经把常见分组吸顶能力准备好了。你要做的不是重新发明一遍,而是把它需要的结构喂对。

所以这里的正确心态应该是:

  • 先把分组关系建好
  • 再把分组头定义清楚
  • 最后让 List 开启 sticky

顺序别反。

如果只看一个页面来练,我更推荐 CityList

HomePage 里的分组吸顶是入门版,CityList 则更像标准模板。

它的核心结构是这样的:

List({ scroller: this.cityScroller }) {
  ListItemGroup({ header: this.itemHead($r('app.string.current_city')) }) {
    ListItem() {
      Text(this.currentCity)
    }
  }

  ListItemGroup({ header: this.itemHead($r('app.string.popular_cities')) }) {
    ForEach(this.hotCities, (item: string) => {
      ListItem() {
        this.textContent(item)
      }
    })
  }

  ForEach(this.groupWorldList, (item: string) => {
    ListItemGroup({ header: this.itemHead(item) }) {
      ForEach(this.getCitiesWithGroupName(item), (cityItem: City) => {
        ListItem() {
          this.textContent(cityItem.city)
        }
      })
    }
  })
}
.sticky(StickyStyle.Header)

这页为什么特别适合拿来练?

因为结构太典型了:

  • 上面是特殊分组,比如当前城市、热门城市
  • 下面是按字母切开的标准分组
  • 所有内容都统一放进 ListItemGroup
  • 吸顶逻辑完全交给 sticky

这是一种特别干净的示范。

你自己复刻时,最少得满足这三个条件

如果你准备照着做一个类似页面,至少要保证这三点同时成立:

  • 外层容器是 List
  • 每一组内容都用 ListItemGroup
  • 组头通过 header 提供,并在列表上开启 .sticky(StickyStyle.Header)

少任何一个,效果都不会完整。

别在这一步偷懒。

有个细节很多人真会省,但我建议别省

那就是分组头的背景色。

你可能会觉得,标题都显示出来了,背景色有没有都差不多。真不是。

分组头一旦吸到顶部,它其实是在内容上方“悬着”的。背景如果不明确,下面列表内容会透出来,页面看着立刻廉价很多。这个项目里不管是景区标题还是城市标题,都专门给了背景色,这不是装饰,是经验。

这种结构以后能直接套到哪类页面

别把它只当城市页技巧。

这一套你以后完全可以原样迁过去:

  • 城市选择页按字母分组
  • 订单页按日期分组
  • 消息中心按类型分组
  • 商品列表按品类分组
  • 文档中心按月份分组

它们本质一样,都是“同类内容归一组,组头负责提示,滚动时让组头持续在线”。

我最想提醒小白的三个误区

误区一: 以为吸顶一定要自己算滚动距离

普通分组吸顶真不用。先把结构写对,再谈更复杂的动画需求。

误区二: 把标题直接塞进 ListItem

视觉上看着像有标题,但框架并不知道它是“组头”,自然也不会帮你吸顶。

误区三: 标题区样式写得太敷衍

高度、背景、间距全乱来,最后吸顶动作虽然发生了,页面质感却很差。

给你一个非常适合练手的小作业

你可以在 CityList 上先做两件小事:

  • 给字母标题提一点对比度,比如更粗的字重或者更明确的背景
  • 给城市项加分割线或者卡片样式

这两个改动不会碰核心逻辑,但能让你很直接地感受到: 列表结构没变,页面气质却能差很多。

最后一句

分组吸顶这件事,真正难的从来不是 API,而是你有没有先把“分组”这件事想清楚。

一旦你接受“组用 ListItemGroup 管,吸顶交给 sticky”这套思路,很多原本看起来像高阶页面的东西会突然变简单。别自己先把它想复杂了,框架其实已经替你省了不少力气。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐