源码获取

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

ManagerPage 这页特别像很多人第一次做内容频道页时会写出来的“事故现场”。

顶部有搜索区,中间有头图,下面是一排内容分类,再往下还有内容列表,最底下甚至还有一层底部导航。东西一多,最容易发生的事不是不会写,而是层级写着写着就糊了。

所以这篇我不打算先讲细节,我先带你把骨架掰顺。

A technical blueprint diagram illustrating the lay

因为这类页面真正难的地方,从来都不是某个组件名,而是你有没有先把“谁包谁、谁压谁、谁滚谁”这几件事想明白。

先别被页面热闹吓到,它其实就四层

如果你直接盯源码,很容易觉得这页像一团线。

但你只要按层去看,它就突然没那么可怕了。ManagerPage 你可以先拆成四层:

  1. 最外层是 NavDestination
  2. 里面包了一层底部 Tabs
  3. 第一个底部页签内部用 Stack 叠了两块区域
  4. Stack 上面是搜索区,下面是滚动内容区

核心结构大概就是这样:

NavDestination() {
  Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
    TabContent() {
      Stack({ alignContent: Alignment.Top }) {
        Scroll(this.scrollController) {
          Column() {
            Image($r('app.media.pic5'))
            Column() {
              Row({ space: 16 }) {
                // 内容分类 Tab
              }
              Tabs({ barPosition: BarPosition.Start, controller: this.contentTabController }) {
                // 内容区
              }
            }
          }
        }

        Row() {
          // 顶部搜索区
        }
      }
    }
    .tabBar(this.tabBuilder($r('app.media.mine'), $r('app.string.tabBar1'), 0))
  }
}

别急着记代码,先记这句更有用的话: 复杂页面先拆层,不要先抠细节。

为什么这里一定要用 Stack

很多人第一次看到 Stack 会觉得它有点多余,心里会冒出一句: 直接 Column 不行吗?

真不太行。

因为这个页面的搜索区不是普通内容块,它更像一个“压在内容上方”的区域。用户往下滑时,下面内容在动,但顶部搜索区需要稳定地待在最上面。

An ink-style comparison diagram contrasting two ty

这时候 Stack({ alignContent: Alignment.Top }) 就特别顺手。它不是在炫技,而是在明确层级:

  • 下层是可以滚动的内容
  • 上层是固定在顶部的搜索区

这层关系一旦理顺,页面的气质就会稳定很多。

底部 Tabs 和中间那排文字 Tab,根本不是一回事

这是 ManagerPage 最容易把人看乱的点。

项目最外层先放了一组底部标签栏:

Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
  TabContent() {
    // 首页内容
  }
  .tabBar(this.tabBuilder($r('app.media.mine'), $r('app.string.tabBar1'), 0))

  TabContent() {
    Text($r('app.string.tabBar2_content'))
  }
  .tabBar(this.tabBuilder($r('app.media.mine'), $r('app.string.tabBar2'), 1))
}
.barHeight(56)
.vertical(false)
.barMode(BarMode.Fixed)

这组 Tabs 是整页级别的大导航。你可以把它理解成 App 底部模块切换。

而页面中间那排“关注、热搜、推荐、更多”,它切的是当前页面内部的内容分类,不是整个页面模块。两层导航看着都叫 Tab,但职责完全不同。

这个边界你如果不先分清,后面读联动逻辑会特别乱。

中间那排文字 Tab,本质上是一个自定义分类条

项目没有直接用默认的 TabBar 展示内容分类,而是自己画了一排文字:

Row({ space: 16 }) {
  ForEach(this.tabArray, (item: string, index: number) => {
    Text(item)
      .fontColor(this.currentTabIndex === index ? '#0a59f7' : Color.Black)
      .onClick(() => {
        this.contentTabController.changeIndex(index)
        this.currentTabIndex = index
      })
  }, (item: string) => item)
}

A hand-drawn style flowchart detailing the recomme

这段写法特别像真实项目里的频道分类条。

因为很多时候,产品并不想要默认那套系统 Tab 样式,而是希望你自己控制字色、间距、选中状态、甚至交互动效。那最稳的办法,就是像这里一样: 显示层自己画,切换还是交给 TabsController

为什么这页的骨架一定要按顺序搭

我很不建议你一上来就学着把 ManagerPage 一口气写完。

更稳的做法是按下面这个顺序走:

  • 先把最外层底部 Tabs 搭出来
  • 再把第一页的 Stack 结构搭出来
  • 然后把顶部搜索区和滚动内容区分开
  • 最后再往内容区里塞分类条、列表和联动逻辑

这个顺序特别重要。

因为一旦你先去抠内部列表,后面才发现外层滚动层级没搭好,返工会很烦。复杂页面一定先搭骨头,再长肉。

tabBuilder 单独抽出来,其实很像在给后面省命

项目把底部标签栏的每个按钮都统一交给 tabBuilder():

@Builder
tabBuilder(img: Resource, title: Resource, index: number) {
  Column() {
    Image(img)
      .fillColor(this.currentIndex === index ? '#0a59f7' : '#66000000')
    Text(title)
      .fontColor(this.currentIndex === index ? '#0a59f7' : '#66000000')
  }
  .onClick(() => {
    this.currentIndex = index
    this.tabsController.changeIndex(this.currentIndex)
  })
}

这种写法对教程特别友好,也对实战特别友好。

原因很简单: 底部导航一旦统一封装,后面无论你是改图标色、改文字色、改交互,都会轻松很多。你不会为了五个 Tab 去维护五份长得差不多的代码。

小白最该从这页学走的,不是某个属性名

而是一种拆页面的习惯。

这页真正教你的,是下面这几件事:

  • 大导航和小导航要分层
  • 固定顶层区域最好用 Stack
  • 可滚动内容区最好单独收口
  • 自定义分类条和真实内容切换可以分开处理

这一套思路你一旦熟了,后面看复杂频道页、个人主页、内容流详情页,都会顺眼很多。

现在就能做的练手动作

如果你想马上上手,不用一口气全改,先做这三个最稳:

  • 把底部导航从 5 项减成 3 项
  • 把中间内容分类改成你自己的 3 个栏目
  • 把顶部搜索区文案换成你自己的演示词

先做这种不伤筋骨的小改动,你会更容易建立对这页结构的控制感。

最后一句

ManagerPage 这种页面最怕的不是代码多,而是你一开始就没把层级想清楚。

只要你先把“外层导航、叠层结构、顶部区域、滚动内容区”这四件事看顺,后面的嵌套滚动、Tab 联动、自定义列表项就不会再像一团毛线。复杂页面没那么可怕,怕的是你一上来就冲进细节里打架。

Logo

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

更多推荐