HarmonyOS开发:系统UI定制——状态栏与导航栏定制
HarmonyOS开发:系统UI定制——状态栏与导航栏定制
📌 核心要点:状态栏和导航栏是用户感知系统最直接的界面元素,定制它们需要系统签名和窗口管理API的配合,搞懂层级关系是关键。
背景与动机
你做设备定制,客户第一眼看的是啥?不是你的应用功能多强大,而是那个状态栏——左边几个图标、右边几个图标、颜色对不对、电量显示是不是他们要的样式。
状态栏不对,客户就觉得这系统不对。导航栏同理,手势导航还是三键导航、返回键在左还是在右、背景色是透明还是半透明——这些都是设备定制的"门面工程"。
但问题来了:状态栏和导航栏不是你应用的UI,它们是系统级的窗口。普通应用只能通过WindowStage设置沉浸式状态,想改状态栏内容?想自定义导航栏按钮?不好意思,你得有系统签名,还得用系统级API。
这篇就来讲清楚:怎么定制状态栏、怎么改导航栏、怎么让系统UI跟你的产品调性一致。
核心原理
系统UI的窗口层级
鸿蒙的屏幕显示区域从上到下分四层:
┌─────────────────────────┐
│ 状态栏 (StatusBar) │ ← 系统窗口,z-order最高
├─────────────────────────┤
│ │
│ 应用窗口区域 │ ← 应用窗口
│ │
├─────────────────────────┤
│ 导航栏 (NavigationBar) │ ← 系统窗口,z-order次高
└─────────────────────────┘
状态栏和导航栏都是系统级窗口,由SystemUI进程管理。你的应用窗口夹在中间,默认情况下是被它们"挤压"的。
定制系统UI,本质上是跟SystemUI进程打交道。有两种方式:
- 应用内定制:通过
WindowAPI设置当前窗口的状态栏/导航栏样式,只影响自己的页面 - 系统级定制:修改SystemUI源码或覆盖系统资源,影响全局
flowchart TD
A[系统UI定制] --> B{定制范围}
B -->|应用内| C[Window API]
B -->|系统级| D[SystemUI源码修改]
C --> C1[setWindowLayoutFullScreen]
C --> C2[状态栏颜色/透明度]
C --> C3[导航栏颜色/透明度]
C --> C4[状态栏图标亮暗色]
D --> D1[状态栏布局覆盖]
D --> D2[导航栏布局覆盖]
D --> D3[系统主题资源替换]
D --> D4[系统签名+特权API]
classDef scope fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
classDef appMethod fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
classDef sysMethod fill:#fff3e0,stroke:#ff9800,color:#e65100
class B scope
class C,C1,C2,C3,C4 appMethod
class D,D1,D2,D3,D4 sysMethod
状态栏的构成
状态栏分左右两个区域:
- 左侧:通知图标区域,显示通知小图标
- 右侧:系统图标区域,显示Wi-Fi、信号、电量、时间等
右侧这些系统图标是硬编码在SystemUI里的,普通应用改不了。但你可以通过系统属性控制某些图标的显隐,比如隐藏运营商名称、隐藏蓝牙图标等。
导航栏的三种模式
| 模式 | 说明 | 定制空间 |
|---|---|---|
| 三键导航 | 返回、主页、最近任务 | 按钮图标、颜色、顺序 |
| 手势导航 | 底部手势条 | 手势条颜色、高度 |
| 悬浮导航 | 可拖拽的虚拟按键 | 位置、大小、透明度 |
代码实战
基础用法:应用内状态栏与导航栏定制
最常见的场景:你的应用需要沉浸式体验,状态栏要变透明,内容延伸到状态栏下方。
// common/WindowManager.ets - 窗口管理工具
import window from '@ohos.window';
class WindowManager {
private tag: string = 'WindowManager';
// 设置沉浸式状态栏
async setImmersiveStatusBar(windowStage: window.WindowStage): Promise<void> {
try {
const mainWindow = await windowStage.getMainWindow();
// 设置全屏布局,让内容延伸到状态栏区域
await mainWindow.setWindowLayoutFullScreen(true);
// 获取状态栏和导航栏区域
const avoidArea = await mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const statusBarHeight = avoidArea.topRect.height;
// 设置状态栏透明
const systemBarProperties: window.SystemBarProperties = {
statusBarColor: '#00000000', // 透明背景
statusBarContentColor: '#000000', // 深色图标
navigationBarColor: '#00000000', // 透明背景
navigationBarContentColor: '#000000', // 深色按钮
};
await mainWindow.setWindowSystemBarProperties(systemBarProperties);
console.info(this.tag, `沉浸式设置成功,状态栏高度: ${statusBarHeight}px`);
} catch (err) {
console.error(this.tag, `沉浸式设置失败: ${JSON.stringify(err)}`);
}
}
// 设置状态栏颜色(非透明)
async setStatusBarColor(
windowStage: window.WindowStage,
color: string,
isLightContent: boolean
): Promise<void> {
try {
const mainWindow = await windowStage.getMainWindow();
const systemBarProperties: window.SystemBarProperties = {
statusBarColor: color,
statusBarContentColor: isLightContent ? '#ffffff' : '#000000',
navigationBarColor: color,
navigationBarContentColor: isLightContent ? '#ffffff' : '#000000',
};
await mainWindow.setWindowSystemBarProperties(systemBarProperties);
console.info(this.tag, `状态栏颜色设置成功: ${color}`);
} catch (err) {
console.error(this.tag, `状态栏颜色设置失败: ${JSON.stringify(err)}`);
}
}
}
export default new WindowManager();
进阶用法:系统级状态栏定制
系统级定制需要系统签名。你可以通过系统属性控制状态栏行为,或者直接覆盖SystemUI的资源文件。
// common/SystemUIManager.ets - 系统UI管理(需要系统签名)
import systemParameter from '@ohos.systemParameter';
import window from '@ohos.window';
class SystemUIManager {
private tag: string = 'SystemUIManager';
// 隐藏/显示状态栏(系统级)
async setStatusBarVisible(visible: boolean): Promise<boolean> {
try {
// 通过系统属性控制状态栏显隐
await systemParameter.set('persist.sys.statusbar.enable', visible ? '1' : '0');
console.info(this.tag, `状态栏${visible ? '显示' : '隐藏'}设置成功`);
return true;
} catch (err) {
console.error(this.tag, `状态栏显隐设置失败: ${JSON.stringify(err)}`);
return false;
}
}
// 设置导航栏模式(系统级)
async setNavigationBarMode(mode: 'three_key' | 'gesture' | 'float'): Promise<boolean> {
try {
const modeMap = {
'three_key': '0',
'gesture': '1',
'float': '2'
};
await systemParameter.set('persist.sys.navigationmode', modeMap[mode]);
console.info(this.tag, `导航栏模式设置成功: ${mode}`);
return true;
} catch (err) {
console.error(this.tag, `导航栏模式设置失败: ${JSON.stringify(err)}`);
return false;
}
}
// 获取状态栏高度(系统级精确获取)
async getStatusBarHeight(): Promise<number> {
try {
const heightStr = systemParameter.get('const.statusbar.height');
const height = parseInt(heightStr, 10);
return isNaN(height) ? 0 : height;
} catch (err) {
console.error(this.tag, `获取状态栏高度失败: ${JSON.stringify(err)}`);
return 0;
}
}
// 设置状态栏图标方向(亮色/暗色,系统级)
async setStatusBarIconDirection(isLight: boolean): Promise<boolean> {
try {
const allWindows = await window.getAllWindowNames();
for (const winName of allWindows) {
try {
const win = await window.findWindow(winName);
const props: window.SystemBarProperties = {
statusBarContentColor: isLight ? '#ffffff' : '#000000',
};
await win.setWindowSystemBarProperties(props);
} catch (e) {
// 某些窗口可能无法设置,跳过
continue;
}
}
return true;
} catch (err) {
console.error(this.tag, `设置图标方向失败: ${JSON.stringify(err)}`);
return false;
}
}
}
export default new SystemUIManager();
完整示例:自适应状态栏页面
一个根据页面背景色自动调整状态栏样式的完整页面:
// pages/AdaptiveUIPage.ets - 自适应状态栏页面
import window from '@ohos.window';
@Entry
@Component
struct AdaptiveUIPage {
@State currentBgIndex: number = 0;
@State statusBarHeight: number = 0;
@State navBarHeight: number = 0;
// 预设的页面主题
private themes = [
{ name: '浅色主题', bg: '#f5f5f5', barColor: '#ffffff', isLight: false },
{ name: '深色主题', bg: '#1a1a2e', barColor: '#16213e', isLight: true },
{ name: '品牌主题', bg: '#e8f0fe', barColor: '#1976d2', isLight: true },
{ name: '沉浸主题', bg: '#000000', barColor: '#00000000', isLight: true },
];
aboutToAppear() {
this.loadAvoidAreaSize();
}
// 获取安全区域尺寸
async loadAvoidAreaSize() {
try {
const mainWindow = window.findWindow('AdaptiveUIPageWindow');
const avoidArea = await mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
this.statusBarHeight = avoidArea.topRect.height;
const navAvoidArea = await mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION);
this.navBarHeight = navAvoidArea.bottomRect.height;
} catch (err) {
console.error('获取安全区域失败', JSON.stringify(err));
}
}
// 切换主题并更新状态栏
async switchTheme(index: number) {
this.currentBgIndex = index;
const theme = this.themes[index];
try {
const mainWindow = window.findWindow('AdaptiveUIPageWindow');
await mainWindow.setWindowLayoutFullScreen(true);
const props: window.SystemBarProperties = {
statusBarColor: theme.barColor,
statusBarContentColor: theme.isLight ? '#ffffff' : '#000000',
navigationBarColor: theme.barColor,
navigationBarContentColor: theme.isLight ? '#ffffff' : '#000000',
};
await mainWindow.setWindowSystemBarProperties(props);
} catch (err) {
console.error('切换主题失败', JSON.stringify(err));
}
}
build() {
Column() {
// 状态栏占位
Row()
.width('100%')
.height(this.statusBarHeight)
// 页面内容
Column() {
Text(this.themes[this.currentBgIndex].name)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(this.themes[this.currentBgIndex].isLight ? '#ffffff' : '#333333')
.margin({ bottom: 32 })
// 主题切换按钮组
ForEach(this.themes, (theme: Record<string, Object>, index: number) => {
Button(theme.name as string)
.width('80%')
.height(48)
.backgroundColor(index === this.currentBgIndex ? '#ff6f00' : '#424242')
.fontColor('#ffffff')
.borderRadius(24)
.margin({ bottom: 12 })
.onClick(() => this.switchTheme(index))
}, (theme: Record<string, Object>, index: number) => `${index}`)
// 状态信息
Column() {
Text(`状态栏高度: ${this.statusBarHeight}px`)
.fontSize(14)
.fontColor(this.themes[this.currentBgIndex].isLight ? '#cccccc' : '#999999')
Text(`导航栏高度: ${this.navBarHeight}px`)
.fontSize(14)
.fontColor(this.themes[this.currentBgIndex].isLight ? '#cccccc' : '#999999')
}
.margin({ top: 32 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
// 导航栏占位
Row()
.width('100%')
.height(this.navBarHeight)
}
.width('100%')
.height('100%')
.backgroundColor(this.themes[this.currentBgIndex].bg as string)
}
}
这个页面的核心逻辑:切换主题时,同时更新页面背景色和状态栏/导航栏的颜色与图标方向。沉浸主题下状态栏完全透明,内容从屏幕最顶部开始渲染。
踩坑与注意事项
坑一:沉浸式布局内容被状态栏遮挡
设置了setWindowLayoutFullScreen(true)之后,你的内容确实延伸到了状态栏下方,但状态栏也确实把你的内容挡住了。
解决办法:给顶部加一个跟状态栏等高的占位组件。但这个高度不是固定的,不同设备状态栏高度不一样。必须通过getWindowAvoidArea动态获取。
// 错误:硬编码状态栏高度
Row().height(48) // 在某些设备上可能不够
// 正确:动态获取状态栏高度
const avoidArea = await mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const statusBarHeight = avoidArea.topRect.height;
Row().height(statusBarHeight)
坑二:状态栏透明但图标看不见
深色背景+深色图标,或者浅色背景+浅色图标——都是灾难。
记住这个搭配:
- 浅色背景 → 深色图标(
statusBarContentColor: '#000000') - 深色背景 → 浅色图标(
statusBarContentColor: '#ffffff')
但有些场景背景色是渐变的或者有图片,不好判断亮暗。这时候可以取背景色中间位置的像素值算亮度:
// 简单的亮度判断
function isLightColor(hexColor: string): boolean {
const r = parseInt(hexColor.slice(1, 3), 16);
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
// ITU-R BT.601亮度公式
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
return luminance > 128;
}
坑三:页面切换时状态栏样式闪烁
从页面A(浅色状态栏)切到页面B(深色状态栏),中间会闪一下。因为setWindowSystemBarProperties是异步的,页面已经显示了但状态栏还没改过来。
解决办法:在页面aboutToAppear里提前设置,不要等到onPageShow。
坑四:横竖屏切换后安全区域尺寸变了
设备旋转后,状态栏可能消失(某些应用横屏全屏模式),导航栏位置也可能变化。你之前拿到的statusBarHeight就过期了。
需要监听窗口尺寸变化:
// 监听窗口尺寸变化,重新获取安全区域
mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
this.statusBarHeight = data.area.topRect.height;
}
if (data.type === window.AvoidAreaType.TYPE_NAVIGATION) {
this.navBarHeight = data.area.bottomRect.height;
}
});
坑五:系统属性修改不生效
通过systemParameter.set修改系统UI相关属性后,发现状态栏没有变化。原因是:
- 有些属性是
persist.开头的,修改后需要重启SystemUI进程才生效 - 有些属性是
ro.开头的,只读,根本改不了 - SystemUI可能没有监听你修改的那个属性
确认属性是否可写、是否需要重启,最好先查SystemUI源码里的属性监听逻辑。
HarmonyOS 6适配说明
HarmonyOS 6在系统UI定制方面有几个变化:
- 新的SystemBarProperties字段:新增
statusBarIconDirection字段,直接控制图标亮暗,不再需要手动设置颜色。这解决了深色/浅色图标判断不准的问题。
// HarmonyOS 6 新增字段
const props: window.SystemBarProperties = {
statusBarColor: '#1976d2',
// 新增:直接指定图标方向
statusBarIconDirection: 'light', // 'light' 或 'dark'
navigationBarColor: '#1976d2',
navigationBarIconDirection: 'light',
};
-
窗口避让区域细分:
AvoidAreaType新增了TYPE_CUTOUT(刘海屏区域)和TYPE_KEYBOARD(软键盘区域),可以更精确地处理不同类型的避让。 -
状态栏自定义布局API:HarmonyOS 6新增了
StatusBarCustomization接口,系统签名应用可以完全自定义状态栏的布局,不再局限于改颜色和图标方向。 -
导航栏手势区域增强:手势导航的底部手势区域可以通过
GestureNavigationArea接口自定义响应范围,避免误触。
适配建议:优先使用新的statusBarIconDirection字段替代手动设置颜色,更可靠也更简洁。
总结
系统UI定制分两个层次:应用内定制用Window API就够了,系统级定制需要系统签名和SystemUI源码修改。搞清楚你的需求属于哪个层次,别走弯路。
核心要点回顾:
- 沉浸式布局 =
setWindowLayoutFullScreen(true)+ 顶部占位 - 状态栏图标亮暗必须跟背景色搭配,否则看不见
- 安全区域尺寸必须动态获取,不能硬编码
- 系统级定制需要系统签名,通过系统属性或覆盖资源实现
- 横竖屏切换后安全区域会变,需要监听更新
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ Window API不难,系统级定制需要理解SystemUI架构 |
| 使用频率 | ⭐⭐⭐⭐⭐ 几乎每个应用都要处理状态栏适配 |
| 重要程度 | ⭐⭐⭐⭐ 直接影响用户体验和视觉一致性 |
- 点赞
- 收藏
- 关注作者
评论(0)