源码获取

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

很多人第一次写嵌套滚动,表面上是不会配 API,实际上是脑子里没有一个清楚的画面: 用户这一滑,到底应该谁先动。

这件事如果没想明白,页面就会出现特别典型的几种毛病: 上下滑发僵、父子容器抢手势、顶部区域收得很怪、Tab 看着切了,内容却没跟上。

ManagerPage 这里给的实现,我挺喜欢。它不是那种看起来很炫、实则不太好学的写法,而是一套很标准、很适合小白建立感觉的方案。

先别上来研究参数,先看这页到底谁在滚

这页不是一层滚动,而是两层:

  • 外层是 Scroll
  • 内层是 List

A hand-drawn sketch-notes style flowchart illustra

而且这个内层 List 还被放在 Tabs 里。关键结构长这样:

Scroll(this.scrollController) {
  Column() {
    Image($r('app.media.pic5'))
    Column() {
      Row({ space: 16 }) {
        // 内容分类
      }

      Tabs({ barPosition: BarPosition.Start, controller: this.contentTabController }) {
        TabContent() {
          List({ space: 10, scroller: this.listScroller }) {
            CustomListItem({ imgUrl: $r('app.media.pic1'), title: $r('app.string.manager_content') })
          }
          .nestedScroll({
            scrollForward: NestedScrollMode.PARENT_FIRST,
            scrollBackward: NestedScrollMode.SELF_FIRST
          })
        }
      }
      .barHeight(0)
      .height('calc(100% - 100vp)')
    }
  }
}

A split-screen sketch-notes infographic demonstrat

你现在不用先背下来,先在脑子里想象这个画面:

  • 用户刚开始上滑时,先把头图和上面的区域收起来
  • 收到一定程度后,内部内容列表开始接管滚动
  • 反向下拉时,内部列表先回到顶部
  • 再往下,外层区域才慢慢露出来

如果这个画面你能想清楚,后面那几个参数就不再只是死记硬背。

nestedScroll 真正解决的,是“滚动权交给谁”

项目里最关键的一段就是这个:

.nestedScroll({
  scrollForward: NestedScrollMode.PARENT_FIRST,
  scrollBackward: NestedScrollMode.SELF_FIRST
})

很多教程会直接解释成“父优先、子优先”。这当然没错,但对新手不够落地。

我更建议你直接这么理解:

  • 手指往上推时,先让外层大框架动
  • 手指往下拉时,先让内层内容区自己收回来

这就好懂多了。

为什么这里上滑要 PARENT_FIRST

你想想这页的视觉顺序:

  • 顶部有搜索区
  • 下面有大图
  • 再下面才是内容分类和列表

用户上滑时,一般都会预期先把头部冗余区域滑走,把真正想看的内容顶上来。所以这里写:

scrollForward: NestedScrollMode.PARENT_FIRST

特别合理。

也就是说,先让外层 Scroll 消化这次上滑,等头部区域该收的都收差不多了,再把后续滚动交给内层 List。这就是为什么这个页面看起来比较顺,不会一上来就让内部列表自己乱跑。

为什么回拉要 SELF_FIRST

反过来,当你往下拉时,用户大多会预期先看到当前内容列表自己往回走。

也就是说,先把列表滚回顶部,再逐渐把页面上方的大图和其它区域露出来。

所以这里配的是:

scrollBackward: NestedScrollMode.SELF_FIRST

这个方向如果配反,页面手感很容易变怪。你会明显感觉到“明明是在看内容,结果一拉就先动外层”,用户会很别扭。

中间那排文字 Tab,为什么要和内容双向同步

项目里内容分类条的点击逻辑是这样:

Text(item)
  .onClick(() => {
    this.contentTabController.changeIndex(index)
    this.currentTabIndex = index
  })

A two-panel sketch-notes comparison of Tabs implem

而内部 Tabs 切换完以后,又会回写当前索引:

.onChange((index: number) => {
  this.currentTabIndex = index
})

这套写法我很推荐你记住。

因为它解决的是一个很现实的问题: 你不能只让“点击标签”改内容,也不能只让“内容切换”自己闷头变。显示层和真实内容状态得互相对上。

不然很容易出现这种糟糕场景: 内容已经切到下一页了,顶部文字却还亮着上一项。

barHeight(0) 不是小细节,它是在主动隐藏默认 TabBar

很多人第一次看这句会直接略过:

.barHeight(0)

其实它很重要。

因为这页已经自己画了一排内容分类文字,如果系统默认的 TabBar 还留着,界面就会出现两层分类条,看起来很重复,也很不专业。

所以这里把默认 TabBar 高度压成 0,相当于告诉框架: 切换逻辑我还要,但显示层我自己接管。

这种做法在真实项目里特别常见,尤其是产品对 Tab 样式有定制要求的时候。

calc(100% - 100vp) 真不是为了写得高级

这一句看起来有点唬人:

.height('calc(100% - 100vp)')

但它干的事情很务实,就是给内部内容区留出一个“剩余可用高度”。

因为这页上面已经有头图和分类区,如果你不把可滚动区域的高度边界想清楚,内部列表要么滚不顺,要么根本拿不到正确空间。

复杂页面里,这种剩余空间分配能力特别常见。你越早接受它,后面越不容易被布局问题折腾。

嵌套滚动最容易写崩的,不是难点,而是顺序感

我最常见到的几个坑,基本都和“顺序没想清楚”有关。

外层和内层都能滚,但谁先滚没设计

用户一滑,页面就会表现得很拧巴,看着像能动,其实一点都不好用。

只写了点击 Tab 的切换,没写 onChange

这种问题最烦,因为表面上像是“偶尔不对”,本质上却是状态同步没闭环。

默认 TabBar 和自定义分类条一起出现

功能不一定坏,但页面一下子就显得很糙。

内容区高度没收好

这种情况特别容易导致“页面看着正常,结果内部列表死活滚不起来”。

最适合你现在做的两个实验

如果你真想把这一页吃透,我建议你别只看,自己动两刀:

  • scrollForward 改成 SELF_FIRST,感受一下页面手感怎么变
  • 给第二个、第三个 TabContent 也补上真正的列表,而不是只放一行文本

这两个实验都不难,但特别能帮你建立对嵌套滚动的直觉。

最后一句

嵌套滚动这件事,真正难的不是参数名字,而是你有没有先想清楚: 用户这一滑,滚动权到底该先交给谁。

ManagerPage 这页给的答案很标准,也很适合当模板。你把这页练顺以后,后面再做频道页、详情页、评论区、个人主页这种多层滚动页面,心里会稳很多。

Logo

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

更多推荐