HarmonyOS社交通讯应用开发 41: 浏览页架构与底部导航
浏览页架构与底部导航

引言
在示例工程中,用户从首页点击"浏览"按钮后,会通过 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 中声明了 ContentBrowsePage 的 pageSourceFile 与 buildFunction(ContentBrowsePageBuilder),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.ets 的 build() 核心结构如下:
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.iconArr(FooterTab[]),用 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内放入瀑布流组件;BottomTabBarStyle的normal用普通文字色ohos_id_color_text_primary,selected用激活色ohos_id_color_text_primary_activated,选中态视觉由系统色板保证;index === 2是中间的"+"按钮,走自定义addTabBarBuilder:一个 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();
}
windowUtil 是 entry/src/main/ets/utils/WindowUtil.ets 中 WindowUtil 类的实例(通过 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.ets 中 Navigation(this.pageInfos) 与各页面 HdsNavDestination 之间"一栈多用"的协作方式:导航栈全局单例,页面各自消费。
7. 材质系统与沉浸式:从 windowUtil 到 hdsMaterial
细心的读者会发现本文出现了两套"沉浸式":WindowUtil.setImmersiveType 与 hdsMaterial。它们各司其职——前者(entry/src/main/ets/utils/WindowUtil.ets)处理窗口层的沉浸:隐藏系统装饰、调整装饰按钮样式、监听窗口状态变化;后者(@kit.UIDesignKit 的 hdsMaterial)处理组件层的材质:给标题栏、标签栏叠加 IMMERSIVE/ADAPTIVE 材质与 SMOOTH/EXQUISITE 等级,形成毛玻璃、光感等视觉层次。窗口负责"让内容真正铺满屏幕",材质负责"让内容与系统 UI 和谐共处",两者叠加才构成完整的沉浸式体验。aboutToAppear 中 hdsMaterial.getSystemMaterialTypes() 的能力探测,正是为了保证这一体验在低端或老旧设备上能够优雅降级。
小结
浏览页架构可以用一句话概括:HdsNavDestination 管骨架、HdsTabs 管导航、WaterFlowContentComponent 管内容、FooterTabData 管数据。本文重点拆解了底部导航的四个设计决策:
- 用
BottomTabBarStyle + SymbolGlyphModifier表达选中/未选中双态,视觉随系统色板走; - 中间 Tab 用自定义 Builder 做成圆形"+"按钮,形成电商式导航焦点;
@StorageLink + @Watch监听全局语言,一行代码完成 Tab 文案中英文切换;barFloatingStyle实现沉浸式悬浮胶囊标签栏,并用distributionOSApiVersion分支保证低版本系统可用。
下一篇将深入第一个 Tab 的核心内容——瀑布流 WaterFlow 组件,看看首页内容是如何布局与加载的。
更多推荐



所有评论(0)