浏览页架构与底部导航

img

引言

在示例工程中,用户从首页点击"浏览"按钮后,会通过 pageInfos.pushPathByName('ContentBrowsePage', null, true) 进入浏览页。浏览页是整个"社交通讯协同"体验的内容入口:顶部是吸顶的标题栏与搜索入口,中间是可无限滚动的瀑布流内容,底部则是五个 Tab 组成的沉浸式悬浮导航栏。

本文聚焦浏览页的整体架构,重点拆解底部导航的实现。浏览页的代码入口是 entry/src/main/ets/view/contentBrowse/ContentBrowsePage.ets,页面骨架只有三层:HdsNavDestination(华为设计系统提供的导航容器)→ HdsTabs(底部 Tab 容器)→ TabContent(各 Tab 的内容)。其中第一个 Tab(首页)承载了瀑布流组件 WaterFlowContentComponent,其余 Tab 在本工程中仅保留导航占位。

为什么要把"底部导航"单独拿出来讲?因为它的实现并非常见的 Tabs + BottomTabBarStyle 那么简单,还叠加了中英文切换、选中态图标切换、沉浸式悬浮材质、禁止滑动切换、禁止内容切换等多个细节。逐层拆开后,你会发现每个 API 都在回答一个明确的交互问题。

知识点讲解

1. HdsNavDestination:带设计规范的路由容器

HdsNavDestination 是 UIDesignKit(@kit.UIDesignKit)提供的页面容器组件,它对应 Navigation 体系中的 NavDestination,但内置了华为设计系统(HDS)的规范:标题栏、返回按钮、滚动效果、沉浸材质等开箱即用。浏览页通过系统路由表注册:entry/src/main/resources/base/profile/route_map.json 中声明了 ContentBrowsePagepageSourceFilebuildFunctionContentBrowsePageBuilder),Navigation 通过 pushPathByName('ContentBrowsePage') 即可按名跳转。

2. HdsTabs 与 BottomTabBarStyle

HdsTabs 是 HDS 规范的 Tabs 容器,用法与 ArkUI 的 Tabs 一致:内部写 TabContent 子组件,每个 TabContent 通过 .tabBar() 设置标签。BottomTabBarStyle 是底部标签栏的样式描述,它接收两个参数:

  • normal:未选中态的图标与修饰符;
  • selected:选中态的图标与修饰符;

参数类型是 SymbolGlyphModifier,即用代码方式(而非链式属性)构造系统 Symbol 图标的样式。SymbolGlyphModifier 支持 fontSize()fontColor()renderingStrategy() 等方法,renderingStrategy(SymbolRenderingStrategy.SINGLE) 表示单色渲染,保证图标只随 fontColor 着色。

3. HdsTabsController:控制器的角色

页面里声明了 private controller: HdsTabsController = new HdsTabsController() 并在 HdsTabs({ controller: this.controller }) 中传入。控制器的作用是提供命令式 API(如 changeIndex() 切换 Tab、showSubTabBar() 等),让开发者可以在组件树之外驱动 Tab 状态。本项目虽未主动调用控制器的方法,但保留控制器引用是一种良好的工程习惯:后续需要"点击瀑布流回到顶部的同时切回首页 Tab"之类的联动时,无需重构组件接口,直接调用 controller 即可。这也是 ArkUI 中"状态声明式 + 能力命令式"并存设计的一个典型体现。

4. SymbolGlyph:系统符号图标

SymbolGlyph 是鸿蒙的系统符号图标组件,图标资源以 $r('sys.symbol.xxx') 引用,随系统主题变化、支持多色渲染。相比自定义 PNG,Symbol 图标体积小、可随意变色、天然适配深浅色模式,是底部导航这类"需要常态/选中两套视觉"场景的优选。

5. 沉浸式悬浮标签栏(barFloatingStyle)

barFloatingStyle 是 HdsTabs 在 API 60100 版本引入的悬浮标签栏样式:标签栏不再占满底部,而是像"胶囊"一样悬浮在内容之上,并可通过 systemMaterialEffect 指定材质类型与等级,实现毛玻璃、光感等沉浸效果。它是 API 60100 的新能力,因此项目中对它做了版本分支保护。

结合本项目源码分析

1. 页面骨架:三层结构

ContentBrowsePage.etsbuild() 核心结构如下:

build() {
  HdsNavDestination() {
    if (deviceInfo.distributionOSApiVersion >= 60100) {
      HdsTabs({ controller: this.controller }) {
        ForEach(this.iconArr, (item: FooterTab, index: number) => {
          // ...
        }, (index: number) => JSON.stringify(index));
      }
      .scrollable(false)
      .barOverlap(true)
      .barPosition(BarPosition.End)
      .vertical(false)
      .barFloatingStyle({ ... })
      .onContentWillChange(() => {
        return false;
      })
    } else {
      // Fallback to an earlier version
      // 无 barFloatingStyle 的旧版分支,其余配置一致
    }
  }
  .titleBar({ ... })
  .bindToScrollable([this.waterFlowScroller])
  .onShown(() => this.onShown())
  .onHidden(() => this.onHidden())
}

几个容易忽视的点:

  • ForEach 的 keyGenerator 是 (index: number) => JSON.stringify(index),即用下标作为稳定 key,Tab 列表固定不变,用下标做 key 完全够用;
  • .scrollable(false):禁止左右滑动切换 Tab,五个 Tab 中只有首页有真实内容,滑动切换没有意义,直接禁掉避免误触;
  • .barOverlap(true) + .barPosition(BarPosition.End):标签栏悬浮在内容底部(重叠布局),为后面的悬浮胶囊样式做准备;
  • .onContentWillChange(() => false):拦截"内容即将切换"事件并返回 false,即用户点击任何 Tab 都不会真正切换内容。这保证瀑布流始终停留在首页 Tab,其余 Tab 只是视觉占位;
  • HdsNavDestination 外层通过 .bindToScrollable([this.waterFlowScroller]) 把标题栏与瀑布流滚动器绑定,滚动内容时标题栏产生渐变模糊(scrollEffectType: ScrollEffectType.GRADIENT_BLUR)。

2. 五个 Tab 的差异化处理

ForEach 遍历 this.iconArrFooterTab[]),用 index 区分三种形态:

if (index === 0) {
  // 首页:真实内容 + 双态图标
  TabContent() {
    Column() {
      WaterFlowContentComponent({ waterFlowScroller: this.waterFlowScroller })
    }
  }.tabBar(new BottomTabBarStyle({
    normal: new SymbolGlyphModifier(item.icon).fontSize(24)
      .fontColor([$r('sys.color.ohos_id_color_text_primary')])
      .renderingStrategy(SymbolRenderingStrategy.SINGLE),
    selected: new SymbolGlyphModifier(item.iconSelected).fontSize(24)
      .fontColor([$r('sys.color.ohos_id_color_text_primary_activated')])
      .renderingStrategy(SymbolRenderingStrategy.SINGLE)
  }, item.name))
} else if (index === 2) {
  // 中间"+"号:纯按钮形态,无文字
  TabContent().tabBar(this.addTabBar(item.icon))
} else {
  // 其余 Tab:仅图标 + 文字占位
  TabContent().tabBar(new BottomTabBarStyle({
    normal: new SymbolGlyphModifier(item.icon).fontColor([$r('sys.color.ohos_id_color_text_primary')])
      .renderingStrategy(SymbolRenderingStrategy.SINGLE),
    selected: new SymbolGlyphModifier(item.icon).fontColor([$r('sys.color.ohos_id_color_text_primary_activated')])
      .renderingStrategy(SymbolRenderingStrategy.SINGLE)
  }, item.name))
}
  • index === 0 是"首页"Tab,TabContent 内放入瀑布流组件;BottomTabBarStylenormal 用普通文字色 ohos_id_color_text_primaryselected 用激活色 ohos_id_color_text_primary_activated,选中态视觉由系统色板保证;
  • index === 2 是中间的"+"按钮,走自定义 addTabBar Builder:一个 40×40、圆角 20 的 Button 垫底,上面叠一个白色 SymbolGlyph 加号,形成圆形发布按钮的观感,这是电商/社区类应用常见的"突出中间按钮"设计;
@Builder
addTabBar(icon: Resource) {
  Stack() {
    Button()
      .width(40)
      .height(40)
      .borderRadius(20)

    SymbolGlyph(icon)
      .fontSize(24)
      .fontColor([Color.White])
      .renderingStrategy(SymbolRenderingStrategy.SINGLE)
  }
}
  • 其余 Tab(购物、消息、我的)只有 tabBar 没有内容,纯占位。

3. 中英文 Tab 切换:@Watch 驱动的数据重建

Tab 名称与图标不是写死的,而是来自 FooterTabData 视图模型(entry/src/main/ets/viewmodel/FooterTabData.ets)。页面里用 @StorageLink 监听全局语言:

@StorageLink(CommonConstants.LANGUAGE) @Watch('changeTab') language: string = CommonConstants.CHINESE_LANGUAGE;
@State iconArr: FooterTab[] = [];

changeTab() {
  if (this.language.includes(CommonConstants.CHINESE_LANGUAGE)) {
    this.iconArr = new FooterTabData().tabList;
  } else {
    this.iconArr = new FooterTabDataEn().tabList;
  }
}

language 存储在 AppStorage 中,语言变化时 @Watch('changeTab') 自动触发 changeTab(),重新构建 FooterTab[] 并赋给 @State iconArr,UI 随即刷新。FooterTabData 的构造逻辑在 FooterTabData.ets

export class FooterTabData {
  public tabList: FooterTab[] = [];

  constructor() {
    HomeConstants.FOOTER_TOPIC_LIST.forEach((item: string, index: number) => {
      this.tabList.push(new FooterTab(item, HomeConstants.FOOTER_TOPIC_ICONS[index],
        HomeConstants.FOOTER_TOPIC_ICONS_SELECTED[index]));
    });
  }
}

文案定义在 entry/src/main/ets/constants/HomeConstants.ets:中文 ['首页', '购物', '', '消息', '我的'],英文 ['Home', 'Shopping', '', 'Message', 'Me'];图标使用 sys.symbol 全家桶:house_fill(首页)、bag_fill_1(购物)、plus(加号)、ellipsis_message_fill(消息)、person_crop_circle_fill_1(我的)。注意第三个 Tab 的名称是空字符串,因为它的展示交给自定义 addTabBar 完成。

4. 悬浮标签栏与版本分支

aboutToAppear() 里先做两件事:刷新 Tab 数据、探测系统材质能力:

aboutToAppear(): void {
  this.changeTab();
  if (deviceInfo.distributionOSApiVersion >= 60100) {
    let materialTypes: Array<hdsMaterial.MaterialType> = hdsMaterial.getSystemMaterialTypes();
    if (materialTypes.indexOf(hdsMaterial.MaterialType.IMMERSIVE) < 0) {
      this.customMaterialLevel = hdsMaterial.MaterialLevel.SMOOTH;
    } else {
      this.customMaterialLevel = hdsMaterial.MaterialLevel.EXQUISITE;
    }
  }
}

deviceInfo.distributionOSApiVersion 来自 @kit.BasicServicesKit,返回系统 API 版本号。当版本 ≥ 60100 时走新分支:

.barFloatingStyle({
  barBottomMargin: 28,
  barWidth: { smallWidth: 328, mediumWidth: 328, largeWidth: 328 },
  systemMaterialEffect: {
    materialType: hdsMaterial.MaterialType.IMMERSIVE,
    materialLevel: hdsMaterial.MaterialLevel.EXQUISITE
  }
})
  • barBottomMargin: 28:悬浮胶囊距离屏幕底部 28vp;
  • barWidth:在 small/medium/large 三种窗口宽度下统一取 328vp,即胶囊宽度固定,不随屏幕拉伸;
  • systemMaterialEffect:指定 IMMERSIVE 材质 + EXQUISITE 等级,形成通透的沉浸毛玻璃效果。

hdsMaterial.getSystemMaterialTypes() 返回系统支持的材质类型列表,若设备不支持 IMMERSIVE,则回退到 SMOOTH 等级,保证标题栏材质也能正确降级(customMaterialLevel 用在 titleBar 的 systemMaterialEffect 中)。

旧版本分支(API < 60100)去掉 barFloatingStyle 和材质参数,仅保留 scrollable(false)barOverlap(true) 等通用配置,这就是"双版本分支"的全部含义:用能力检测代替版本判断,新 API 新体验,旧系统降级可用

5. 沉浸式窗口与生命周期钩子

浏览页进入时切换沉浸式窗口(隐藏系统装饰),离开时恢复:

onShown(): void {
  this.windowUtil?.setImmersiveType(ImmersiveType.IMMERSIVE);
  this.windowUtil?.startWindowStatusListener();
}

onHidden(): void {
  this.windowUtil?.setImmersiveType(ImmersiveType.NORMAL);
  this.windowUtil?.release();
}

windowUtilentry/src/main/ets/utils/WindowUtil.etsWindowUtil 类的实例(通过 AppStorage 共享)。setImmersiveType(ImmersiveType.IMMERSIVE) 内部会调用 setWindowDecorVisible(false) 隐藏系统标题栏装饰、setWindowDecorHeight(56) 设置装饰区高度、setDecorButtonStyle 调整按钮样式;退出时恢复 NORMAL 并解绑 windowStatusChange 监听。配合 HdsNavDestination.padding({ top: this.statusBarHeight, bottom: 版本判断 }),保证内容避开状态栏与导航条。

6. 路由注册与导航关系

浏览页本身也是路由体系的一员。entry/src/main/resources/base/profile/route_map.json 中声明了三个页面,浏览页与详情页都在其中:

{
  "name": "ContentBrowsePage",
  "pageSourceFile": "src/main/ets/view/contentBrowse/ContentBrowsePage.ets",
  "buildFunction": "ContentBrowsePageBuilder"
}

首页 Index.ets 通过 this.pageInfos.pushPathByName('ContentBrowsePage', null, true) 进入浏览页;浏览页内点击卡片则用 pushPath({ name: 'ContentDetailSamplePage', param: ... }) 进入详情页。ContentBrowsePage 里通过 @StorageLink('pageInfos') pageInfos: NavPathStack 拿到与首页同一个导航栈实例,因此三个页面共享一条路由栈,返回手势与返回按钮天然可用。这也是 Index.etsNavigation(this.pageInfos) 与各页面 HdsNavDestination 之间"一栈多用"的协作方式:导航栈全局单例,页面各自消费。

7. 材质系统与沉浸式:从 windowUtil 到 hdsMaterial

细心的读者会发现本文出现了两套"沉浸式":WindowUtil.setImmersiveTypehdsMaterial。它们各司其职——前者(entry/src/main/ets/utils/WindowUtil.ets)处理窗口层的沉浸:隐藏系统装饰、调整装饰按钮样式、监听窗口状态变化;后者(@kit.UIDesignKithdsMaterial)处理组件层的材质:给标题栏、标签栏叠加 IMMERSIVE/ADAPTIVE 材质与 SMOOTH/EXQUISITE 等级,形成毛玻璃、光感等视觉层次。窗口负责"让内容真正铺满屏幕",材质负责"让内容与系统 UI 和谐共处",两者叠加才构成完整的沉浸式体验。aboutToAppearhdsMaterial.getSystemMaterialTypes() 的能力探测,正是为了保证这一体验在低端或老旧设备上能够优雅降级。

小结

浏览页架构可以用一句话概括:HdsNavDestination 管骨架、HdsTabs 管导航、WaterFlowContentComponent 管内容、FooterTabData 管数据。本文重点拆解了底部导航的四个设计决策:

  1. BottomTabBarStyle + SymbolGlyphModifier 表达选中/未选中双态,视觉随系统色板走;
  2. 中间 Tab 用自定义 Builder 做成圆形"+"按钮,形成电商式导航焦点;
  3. @StorageLink + @Watch 监听全局语言,一行代码完成 Tab 文案中英文切换;
  4. barFloatingStyle 实现沉浸式悬浮胶囊标签栏,并用 distributionOSApiVersion 分支保证低版本系统可用。

下一篇将深入第一个 Tab 的核心内容——瀑布流 WaterFlow 组件,看看首页内容是如何布局与加载的。

Logo

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

更多推荐