鸿蒙开发实战!手把手教你用 ArkTS 把应用接入状态栏
本文详细介绍了如何将应用接入鸿蒙系统状态栏的开发流程。主要内容包括:1. 状态栏接入的三大核心功能:显示自定义图标、左键点击弹窗、右键菜单操作;2. 9个关键API的详细解析与代码示例,涵盖图标添加/移除、弹窗高度调整、菜单更新等;3. 完整的7步开发流程:从模块导入、Ability创建、配置文件编写,到图标预置、功能配置和状态栏接入;4. 进阶功能实现,如动态更新图标和菜单;5. 常见问题排查指
各位鸿蒙开发者小伙伴们,今天咱们来聊个超实用的技能 —— 把咱们自己开发的应用接入鸿蒙系统的状态栏!先跟大家掰扯掰扯,为啥要做这个事儿?你想啊,状态栏是用户每天解锁手机、用平板时第一眼就能看到的地方,要是能在这儿显示咱们应用的自定义图标,用户点一下图标还能弹出咱们定制的弹窗,右键点一下还有专属菜单,这不光让应用更显眼,用户操作也更方便,体验直接拉满!
不过先跟大家打个预防针:咱们这次用到的 DeskTop Extension Kit(桌面拓展服务) 相关 API,目前只在 2in1 设备(比如鸿蒙的二合一平板)上生效,其他设备暂时用不了哈。别到时候在手机上测试发现没效果,还以为自己代码写错了,白着急半天~
接下来咱们就从 “能实现啥效果”“要用到哪些接口”“具体怎么开发”“遇到问题咋解决” 这几个方面,一步步给大家讲透,每个重点都配了完整的 ArkTS 代码,跟着敲就行,保准你看完就能上手!
一、先搞明白:应用接入状态栏,到底能实现啥?
在写代码之前,咱们得先清楚最终的效果,心里有个谱。按照鸿蒙的状态栏扩展能力,咱们的应用接入后能实现 3 个核心功能:
- 状态栏显示自定义图标:不是系统默认的那种图标,是咱们自己设计的,比如音乐 APP 可以放个小音符,天气 APP 放个小太阳,辨识度拉满;
- 左键点击弹自定义弹窗:用户用鼠标左键点一下状态栏的图标,就能弹出咱们定制的页面,比如音乐 APP 弹窗里显示播放控制,天气 APP 显示实时温度;
- 右键点击显自定义菜单:右键点图标,能弹出咱们设置的菜单选项,比如 “打开应用主页”“查看详情”“退出服务” 这些;
- 应用退出图标自动消失:不用咱们手动处理,只要应用进程销毁了,状态栏的图标就会跟着不见,省得占着状态栏位置。
这几个功能咱们后面都会一个个实现,现在先有个整体印象,接下来咱们先把 “工具” 备好 —— 也就是要用到的 API 接口。
二、核心接口拆解:每个接口是干啥的?附代码示例
要实现状态栏接入,鸿蒙给咱们提供了 9 个核心 API,都在statusBarManager里。我不跟大家玩虚的,每个接口都用大白话讲清楚 “能干嘛”,再给个实际能用的代码示例,参数含义也标明白,保证你一看就懂。
1. addToStatusBar:把应用图标 “钉” 到状态栏(核心中的核心)
作用:这是最关键的接口,没有之一!它的功能就是把咱们配置好的图标、弹窗、菜单信息,一起加到状态栏里,让图标显示出来。
参数:
context:上下文,简单说就是咱们应用当前的运行环境,没它啥操作都做不了;statusBarItem:包含图标、左键弹窗、右键菜单的完整配置信息,是个 “大包裹”。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';
// 假设咱们已经配置好了statusBarItem(后面会讲怎么配置)
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法添加状态栏图标!');
return;
}
// 配置好的状态栏图标信息(后面会详细讲怎么构建)
const statusBarItem: statusBarManager.StatusBarItem = {
icons: { /* 图标配置 */ },
quickOperation: { /* 左键弹窗配置 */ },
statusBarGroupMenu: [ /* 右键菜单配置 */ ]
};
try {
// 调用接口,把图标加到状态栏
statusBarManager.addToStatusBar(context, statusBarItem);
console.info('太棒了!图标成功加到状态栏~');
} catch (error) {
// 万一失败了,打印错误信息,方便排查
console.error(`加图标到状态栏失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
}
2. removeFromStatusBar:把图标从状态栏 “拿掉”
作用:如果咱们想主动移除状态栏的图标(比如用户手动关闭服务时),就用这个接口,不用等应用退出。
参数:只需要context,因为系统能通过上下文找到对应的应用图标。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';
const removeStatusBarIcon = () => {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法移除状态栏图标!');
return;
}
try {
statusBarManager.removeFromStatusBar(context);
console.info('图标成功从状态栏移除~');
} catch (error) {
console.error(`移除状态栏图标失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
}
};
// 调用函数移除图标(比如用户点击“关闭服务”按钮时)
removeStatusBarIcon();
3. updateQuickOperationHeight:调整左键弹窗的高度
作用:咱们给左键弹窗设置了初始高度后,如果后面弹窗里的内容变多了(比如从显示 1 条信息变成 3 条),原来的高度不够用,就用这个接口改高度。
参数:
context:上下文;height:新的高度,单位是 vp(鸿蒙的虚拟像素,适配不同屏幕)。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';
// 把弹窗高度从300vp改成400vp
const updatePopupHeight = () => {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法更新弹窗高度!');
return;
}
const newHeight = 400; // 新的弹窗高度,根据内容调整
try {
statusBarManager.updateQuickOperationHeight(context, newHeight);
console.info(`弹窗高度成功更新为${newHeight}vp~`);
} catch (error) {
console.error(`更新弹窗高度失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
}
};
// 调用函数更新高度(比如弹窗内容加载完成后)
updatePopupHeight();
4. updateStatusBarMenu:更新右键菜单内容
作用:右键菜单不是一成不变的,比如音乐 APP 在播放时显示 “暂停”,暂停时显示 “播放”,这时候就用这个接口动态更新菜单。
参数:
context:上下文;statusBarGroupMenus:新的菜单配置,格式和初始配置一样。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';
// 更新右键菜单(比如把“子菜单项”改成“暂停播放”)
const updateRightMenu = () => {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法更新右键菜单!');
return;
}
// 1. 构建新的子菜单项
const newSubMenus: Array<statusBarManager.StatusBarSubMenuItem> = [];
const subMenuAction: statusBarManager.StatusBarMenuAction = {
abilityName: "EntryAbility" // 点击菜单要跳转的Ability
};
const newSubMenu: statusBarManager.StatusBarSubMenuItem = {
subTitle: "暂停播放", // 新的子菜单文字
menuAction: subMenuAction
};
newSubMenus.push(newSubMenu);
// 2. 构建新的一级菜单
const newMenuItems: Array<statusBarManager.StatusBarMenuItem> = [];
const newMenuItem: statusBarManager.StatusBarMenuItem = {
title: "音乐控制", // 一级菜单文字
subMenu: newSubMenus // 关联新的子菜单
};
newMenuItems.push(newMenuItem);
// 3. 构建新的菜单组
const newGroupMenus: Array<statusBarManager.StatusBarGroupMenu> = [];
newGroupMenus.push(newMenuItems);
// 4. 调用接口更新菜单
try {
statusBarManager.updateStatusBarMenu(context, newGroupMenus);
console.info('右键菜单成功更新~');
} catch (error) {
console.error(`更新右键菜单失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
}
};
// 调用函数更新菜单(比如音乐播放状态变化时)
updateRightMenu();
5. updateStatusBarIcon:更换状态栏图标
作用:应用状态变了,图标也得跟着变,比如 WiFiAPP 在连接成功时显示彩色图标,断开时显示灰色图标,就用这个接口换图标。
参数:
context:上下文;statusBarIcon:新的图标配置,包含白色和黑色两种(适配不同状态栏主题)。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
// 更换状态栏图标(比如从“连接成功”图标换成“断开”图标)
const updateStatusBarIcon = async () => {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法更新图标!');
return;
}
// 1. 获取资源管理器
const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
// 2. 读取新的白色图标(假设新图标叫disconnectWhite.png,放在rawfile里)
const newWhiteFileData = resourceMgr.getRawFileContentSync('disconnectWhite.png');
const newWhiteBuffer = newWhiteFileData.buffer;
const newWhiteImageSource = image.createImageSource(newWhiteBuffer);
const newWhitePixelMap = await newWhiteImageSource.createPixelMap();
// 3. 读取新的黑色图标(disconnectBlack.png)
const newBlackFileData = resourceMgr.getRawFileContentSync('disconnectBlack.png');
const newBlackBuffer = newBlackFileData.buffer;
const newBlackImageSource = image.createImageSource(newBlackBuffer);
const newBlackPixelMap = await newBlackImageSource.createPixelMap();
// 4. 构建新的图标信息
const newIcon: statusBarManager.StatusBarIcon = {
white: newWhitePixelMap,
black: newBlackPixelMap
};
// 5. 调用接口更新图标
try {
statusBarManager.updateStatusBarIcon(context, newIcon);
console.info('状态栏图标成功更新~');
} catch (error) {
console.error(`更新状态栏图标失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
}
};
// 调用函数更新图标(比如WiFi连接状态变化时)
updateStatusBarIcon();
6. on ('statusBarIconClick'):监听图标点击事件
作用:如果咱们没给左键弹窗配置abilityName(后面会讲),就需要自己处理左键点击逻辑,比如点击图标弹出提示,这时候就用这个接口监听点击事件。
参数:
type:固定写'statusBarIconClick';callback:点击后的回调函数,能拿到点击类型(比如左键点击是leftClickType)。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { emitter } from '@kit.BasicServicesKit';
// 1. 定义点击事件的处理函数
private onStatusBarIconClick = (eventData: emitter.EventData) => {
const data = eventData.data as { iconClickType: string };
if (data) {
// 判断点击类型(目前主要是左键点击)
switch (data.iconClickType) {
case 'leftClickType':
console.info('用户左键点击了状态栏图标!');
// 这里写自定义逻辑,比如弹出提示框
promptAction.showToast({ message: '你点击了我的图标~' });
break;
default:
console.info(`未知的点击类型:${data.iconClickType}`);
break;
}
}
};
// 2. 注册监听(比如在页面onPageShow时调用)
const registerIconClickListen = () => {
statusBarManager.on('statusBarIconClick', this.onStatusBarIconClick);
console.info('状态栏图标点击监听已注册~');
};
// 调用注册函数
registerIconClickListen();
7. off ('statusBarIconClick'):注销图标点击监听
作用:监听不能一直挂着,比如页面销毁时不注销,会导致内存泄漏,应用可能卡顿,所以一定要用这个接口注销。
参数:
type:固定'statusBarIconClick';callback:要注销的回调函数,必须和注册时的是同一个。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
// 注销图标点击监听(比如在页面onPageHide或onDestroy时调用)
const unregisterIconClickListen = () => {
statusBarManager.off('statusBarIconClick', this.onStatusBarIconClick);
console.info('状态栏图标点击监听已注销~');
};
// 调用注销函数(页面销毁时)
this.onPageHide(() => {
unregisterIconClickListen();
});
8. on ('rightMenuClick'):监听右键菜单点击事件
作用:右键菜单里的每个选项被点击时,咱们要知道点了哪个,然后执行对应逻辑(比如点击 “打开主页” 就跳转到应用主页),就用这个接口监听。
参数:
type:固定'rightMenuClick';callback:回调函数,能拿到menuCode(菜单标识,用来区分点了哪个菜单)。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { emitter } from '@kit.BasicServicesKit';
import { router } from '@kit.ArkUI';
// 1. 定义右键菜单点击的处理函数
private onRightMenuClick = (eventData: emitter.EventData) => {
const data = eventData.data as { menuCode: string };
if (data) {
const menuCode = data.menuCode;
console.info(`用户点击了右键菜单,menuCode:${menuCode}`);
// 根据menuCode处理不同逻辑(menuCode在配置菜单时设置)
switch (menuCode) {
case 'openHome':
// 点击“打开主页”,跳转到应用主页
router.pushUrl({ url: 'pages/Index' });
break;
case 'showDetail':
// 点击“查看详情”,跳转到详情页
router.pushUrl({ url: 'pages/Detail' });
break;
default:
console.info(`未知的菜单标识:${menuCode}`);
break;
}
}
};
// 2. 注册右键菜单点击监听
const registerRightMenuListen = () => {
statusBarManager.on('rightMenuClick', this.onRightMenuClick);
console.info('右键菜单点击监听已注册~');
};
// 调用注册函数
registerRightMenuListen();
9. off ('rightMenuClick'):注销右键菜单点击监听
作用:和图标点击监听一样,右键菜单的监听也要在不用的时候注销,避免内存泄漏。
参数:
type:固定'rightMenuClick';callback:要注销的回调函数,和注册时一致。
ArkTS 代码示例:
import { statusBarManager } from '@kit.DeskTopExtensionKit';
// 注销右键菜单点击监听(页面销毁时)
const unregisterRightMenuListen = () => {
statusBarManager.off('rightMenuClick', this.onRightMenuClick);
console.info('右键菜单点击监听已注销~');
};
// 页面销毁时调用
this.onPageDestroy(() => {
unregisterRightMenuListen();
});
三、实战开发!从 0 到 1 接入状态栏(7 步走)
讲完了接口,咱们就进入最核心的实战环节!这部分咱们按照 “导入模块→新建 Ability→配置文件→预置图标→配置图标 / 弹窗 / 菜单→接入状态栏→测试” 的步骤来,每一步都给完整代码,跟着敲就行,别偷懒~
第一步:导入必备模块(就像做饭先备食材)
咱们开发任何功能,第一步都是导入需要的模块,状态栏接入也不例外。这里要导入 5 个核心模块,每个模块的作用我都标在注释里了:
// 1. 桌面拓展服务:核心的状态栏管理API都在这儿
import { statusBarManager, StatusBarViewExtensionAbility } from '@kit.DeskTopExtensionKit';
// 2. Ability相关:管理状态栏弹窗的会话(比如加载弹窗页面)
import { UIExtensionContentSession, Want, common } from '@kit.AbilityKit';
// 3. 图片处理:把咱们的图片转换成代码能识别的PixelMap
import { image } from '@kit.ImageKit';
// 4. 资源管理:读取rawfile里的图标资源
import { resourceManager } from '@kit.LocalizationKit';
// 5. 事件监听:处理图标和菜单的点击事件
import { emitter } from '@kit.BasicServicesKit';
// 6. UI相关:比如弹窗提示、路由跳转(可选,根据业务加)
import { promptAction, router } from '@kit.ArkUI';
这一步很简单,但别漏导模块,不然代码里会报 “找不到 xxx” 的错误,到时候查起来麻烦~
第二步:新建 StatusBarViewExtensionAbility 和弹窗页面
咱们前面说过,左键点击图标会弹出自定义弹窗,这个弹窗的 “架子” 就是StatusBarViewExtensionAbility(状态栏扩展 Ability),而弹窗里显示的内容,需要一个单独的页面(比如叫StatusBarPage.ets)。
2.1 新建 MyStatusBarViewAbility.ets(管理弹窗会话)
首先,在entry/src/main/ets目录下,新建一个文件夹statusbarviewextensionability(名字可以自己改,但后面配置路径要对应),然后在这个文件夹里新建MyStatusBarViewAbility.ets文件,代码如下:
import { StatusBarViewExtensionAbility, statusBarManager } from '@kit.DeskTopExtensionKit';
import { UIExtensionContentSession, Want } from '@kit.AbilityKit';
// 定义日志标签,方便调试时区分日志
const TAG = 'MyStatusBarViewExtAbility';
// 继承StatusBarViewExtensionAbility,这是状态栏弹窗的基类
export default class MyStatusBarViewAbility extends StatusBarViewExtensionAbility {
// 1. Ability创建时调用(初始化操作放这儿)
onCreate() {
console.info(TAG, `Ability创建啦!`);
// 这里可以做一些初始化,比如初始化数据、注册监听
}
// 2. 弹窗会话创建时调用(核心!指定弹窗显示哪个页面)
onSessionCreate(want: Want, session: UIExtensionContentSession) {
console.info(TAG, `弹窗会话创建啦,要加载的Ability:${want.abilityName}`);
// 关键代码:指定弹窗显示的页面(路径是pages/StatusBarPage,后面会新建这个页面)
session.loadContent('pages/StatusBarPage');
}
// 3. Ability进入前台时调用(比如弹窗显示时)
onForeground() {
console.info(TAG, `Ability进入前台啦!`);
}
// 4. Ability进入后台时调用(比如弹窗隐藏时)
onBackground() {
console.info(TAG, `Ability进入后台啦!`);
}
// 5. 弹窗会话销毁时调用(释放会话资源)
onSessionDestroy(session: UIExtensionContentSession) {
console.info(TAG, `弹窗会话销毁啦!`);
// 这里可以释放会话相关的资源,比如注销监听
}
// 6. Ability销毁时调用(最后一步,释放所有资源)
onDestroy() {
console.info(TAG, `Ability销毁啦!`);
// 这里释放全局资源,比如取消网络请求、注销全局监听
}
}
重点提醒:
onSessionCreate里的session.loadContent('pages/StatusBarPage')是核心,路径千万别写错!如果页面放在其他目录,比如pages/statusBar/StatusBarPage,那路径就要改成对应的;- 每个生命周期函数的作用都标在注释里了,调试时可以看日志,判断哪个环节出了问题。
2.2 新建 StatusBarPage.ets(弹窗内容页面)
接下来,在entry/src/main/ets/pages目录下,新建StatusBarPage.ets文件,这个页面就是用户左键点击图标时弹出来的内容。咱们先做个简单的测试页面,显示 “这是我的状态栏弹窗” 和一个按钮:
import { Column, Text, Button, StyleSheet, promptAction } from '@kit.ArkUI';
// 弹窗页面的组件
@Entry
@Component
struct StatusBarPage {
build() {
// 垂直布局,居中显示内容
Column({ space: 20, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
// 标题文字
Text('这是我的状态栏弹窗')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.textColor('#333333');
// 测试按钮,点击弹出提示
Button('点击我试试')
.width(200)
.height(40)
.fontSize(16)
.backgroundColor('#007AFF')
.onClick(() => {
promptAction.showToast({
message: '弹窗按钮被点击啦!',
duration: 2000
});
});
}
// 页面宽度占满,高度用咱们后面配置的300vp
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF');
}
}
// 样式(也可以直接写在组件里,分开写更清晰)
const styles = StyleSheet.create({
container: {
width: '100%',
height: '100%',
backgroundColor: '#FFFFFF',
flexDirection: FlexDirection.Column,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center,
padding: 20
},
title: {
fontSize: 18,
fontWeight: FontWeight.Bold,
color: '#333333',
marginBottom: 20
},
button: {
width: 200,
height: 40,
fontSize: 16,
backgroundColor: '#007AFF'
}
});
小技巧:弹窗页面的高度不用在这里写死,后面会通过QuickOperation配置统一设置,这里写height('100%')就行,会自动适配配置的高度。
第三步:配置 module.json5(给 Ability “上户口”)
鸿蒙里的任何 Ability(包括咱们新建的MyStatusBarViewAbility),都必须在module.json5里配置,不然系统找不到它,弹窗也弹不出来。这一步就像给 Ability “上户口”,很关键!
3.1 找到 module.json5 文件
路径是entry/src/main/module.json5,别找错了,不是其他目录下的文件~
3.2 配置 extensionAbilities
在module.json5的module字段下,找到extensionAbilities(如果没有就新建),然后添加如下配置:
{
"module": {
"name": "entry",
"type": "entry",
"srcPath": "./src/main/ets",
"deviceTypes": ["tablet", "2in1"], // 因为只支持2in1设备,这里可以加上
"extensionAbilities": [
{
"name": "MyStatusBarViewAbility", // 必须和咱们新建的Ability类名一致
"icon": "$media:startIcon", // Ability的图标(可以用应用默认图标)
"description": "状态栏扩展Ability,负责显示左键弹窗", // 描述,自己写清楚就行
"type": "statusBarView", // 类型必须是statusBarView,系统才会识别为状态栏扩展
"exported": true, // 必须设为true,允许系统调用这个Ability
"srcEntry": "./ets/statusbarviewextensionability/MyStatusBarViewAbility.ets", // Ability文件的路径,千万别写错!
"skills": [
{
"entities": ["entity.system.extension.statusBarView"], // 声明这是状态栏扩展能力
"actions": ["action.system.statusBarView"]
}
]
}
],
// 其他配置(比如abilities、reqPermissions等)...
}
}
重点检查这 3 个字段:
name:必须和MyStatusBarViewAbility.ets里的类名完全一致,大小写都不能错;type:必须是statusBarView,写别的系统不认;srcEntry:路径要和你实际的文件路径对应,比如你把MyStatusBarViewAbility.ets放在了ets/statusBarExt文件夹下,路径就要改成./ets/statusBarExt/MyStatusBarViewAbility.ets。
这一步错了,后面弹窗肯定弹不出来,所以一定要仔细检查!
第四步:预置状态栏图标资源(给图标 “找个家”)
状态栏图标有两个要求:
- 尺寸必须是24vp*24vp:因为状态栏空间很小,太大了会变形,太小了看不清;
- 要准备两张图:一张白色(适配深色状态栏)、一张黑色(适配浅色状态栏),不然在某些主题下图标会看不见。
4.1 新建 rawfile 文件夹
如果entry/src/main/resources目录下没有rawfile文件夹,就新建一个(名字必须是rawfile,系统只认这个名字)。
4.2 放入图标文件
把准备好的两张 24vp*24vp 的图片放进rawfile里,比如命名为testWhite.png(白色图标)和testBlack.png(黑色图标)。
小提醒:
- 图片格式建议用 PNG,支持透明背景,显示效果更好;
- 文件名可以自己改,但后面代码里要对应上,别到时候代码里读的是
testWhite.png,实际放的是whiteIcon.png,那就会报错。
第五步:配置应用图标信息(把图片转成代码能认的格式)
咱们把图片放进rawfile后,代码不能直接用图片文件,得转换成鸿蒙的PixelMap格式(相当于图片在代码里的 “身份证”),然后构建StatusBarIcon对象,这就是状态栏图标的最终配置。
咱们在index.ets(应用主页)里写这段代码,因为一般是应用启动后就把图标加到状态栏:
import { common } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
import { statusBarManager } from '@kit.DeskTopExtensionKit';
@Entry
@Component
struct Index {
// 应用启动后,在页面加载完成时配置图标并添加到状态栏
async onPageShow() {
await this.configStatusBarIcon();
}
// 配置状态栏图标信息的函数
private async configStatusBarIcon() {
// 1. 获取上下文(关键!没有context啥都干不了)
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取宿主上下文失败,没法配置状态栏图标!');
return;
}
try {
// 2. 获取资源管理器(用来读取rawfile里的图片)
const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
// 3. 读取白色图标,转换成PixelMap
// 3.1 读取rawfile里的testWhite.png文件
const whiteFileData = resourceMgr.getRawFileContentSync('testWhite.png');
if (!whiteFileData) {
console.error('读取白色图标失败,文件可能不存在!');
return;
}
// 3.2 把文件数据转成buffer
const whiteBuffer = whiteFileData.buffer;
// 3.3 创建图片源
const whiteImageSource = image.createImageSource(whiteBuffer);
// 3.4 转成PixelMap(异步操作,所以用await)
const whitePixelMap = await whiteImageSource.createPixelMap();
// 4. 读取黑色图标,转换成PixelMap(和白色图标步骤一样)
const blackFileData = resourceMgr.getRawFileContentSync('testBlack.png');
if (!blackFileData) {
console.error('读取黑色图标失败,文件可能不存在!');
return;
}
const blackBuffer = blackFileData.buffer;
const blackImageSource = image.createImageSource(blackBuffer);
const blackPixelMap = await blackImageSource.createPixelMap();
// 5. 构建StatusBarIcon对象(这就是最终的图标配置)
const statusBarIcon: statusBarManager.StatusBarIcon = {
white: whitePixelMap, // 白色图标,适配深色状态栏
black: blackPixelMap // 黑色图标,适配浅色状态栏
};
console.info('状态栏图标配置成功!接下来配置弹窗和菜单~');
// 这里先存下icon,后面整合的时候要用
this.statusBarIcon = statusBarIcon;
} catch (error) {
console.error(`配置状态栏图标失败!错误信息:${(error as Error).message}`);
}
}
// 后面还要用的变量,先定义在这里
private statusBarIcon?: statusBarManager.StatusBarIcon; // 图标配置
private context?: common.Context; // 上下文
build() {
// 主页内容(这里随便写,比如一个“添加状态栏图标”的按钮)
Column({ space: 30, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center })
.width('100%')
.height('100%') {
Text('应用主页')
.fontSize(24)
.fontWeight(FontWeight.Bold);
Button('添加图标到状态栏')
.width(250)
.height(50)
.fontSize(18)
.backgroundColor('#007AFF')
.onClick(() => {
// 后面会在这里写“整合配置并添加到状态栏”的代码
this.addIconToStatusBar();
});
}
}
}
常见错误排查:
- 如果报 “读取图片失败”,先检查图片文件名是不是和代码里一致,再检查图片是不是在
rawfile文件夹里; - 如果报 “createPixelMap 失败”,检查图片尺寸是不是 24vp*24vp,格式是不是 PNG。
第六步:配置左键弹窗和右键菜单
图标配置好了,接下来要配置两个关键交互:左键弹窗(QuickOperation)和右键菜单(statusBarGroupMenu)。
6.1 配置左键弹窗(QuickOperation)
QuickOperation里包含弹窗的标题、高度、对应的 Ability(就是咱们新建的MyStatusBarViewAbility),代码继续写在index.ets里,新增一个函数:
// 在Index结构体里新增这个函数
private configQuickOperation(): statusBarManager.QuickOperation {
// 构建左键弹窗配置
const quickOperation: statusBarManager.QuickOperation = {
abilityName: "MyStatusBarViewAbility", // 必须和module.json5里配置的Ability名一致
title: "我的应用弹窗", // 弹窗的标题,用户能看到
height: 300, // 弹窗高度,单位vp,根据内容调整(比如内容多就设400)
moduleName: 'entry' // 模块名,就是咱们的entry模块,可选但建议加上,避免冲突
};
return quickOperation;
}
重点:abilityName必须和module.json5里的name完全一致,不然系统找不到对应的 Ability,弹窗就弹不出来!
6.2 配置右键菜单(statusBarGroupMenu)
右键菜单是 “菜单组→一级菜单→子菜单” 的层级结构,咱们先做个简单的菜单:一级菜单叫 “应用操作”,子菜单叫 “打开主页” 和 “查看详情”。代码继续写在index.ets里,新增函数:
// 在Index结构体里新增这个函数
private configRightMenu(): Array<statusBarManager.StatusBarGroupMenu> {
// 1. 配置子菜单1:打开主页
const subMenu1: statusBarManager.StatusBarSubMenuItem = {
subTitle: "打开主页", // 子菜单文字
menuCode: "openHome", // 菜单标识,后面监听点击时用这个区分
menuAction: {
abilityName: "EntryAbility", // 点击子菜单要跳转的Ability(比如应用的主Ability)
moduleName: "entry"
}
};
// 2. 配置子菜单2:查看详情
const subMenu2: statusBarManager.StatusBarSubMenuItem = {
subTitle: "查看详情",
menuCode: "showDetail",
menuAction: {
abilityName: "DetailAbility", // 假设咱们有个DetailAbility,没有的话可以先写EntryAbility
moduleName: "entry"
}
};
// 3. 把两个子菜单放进子菜单数组
const subMenus: Array<statusBarManager.StatusBarSubMenuItem> = [subMenu1, subMenu2];
// 4. 配置一级菜单(关联子菜单)
const menuItem: statusBarManager.StatusBarMenuItem = {
title: "应用操作", // 一级菜单文字
subMenu: subMenus, // 关联上面的子菜单
// 注意:一级菜单的menuAction和subMenu不能都为空,不然菜单显示了点了没反应
// menuAction: { ... } // 如果不想要子菜单,也可以直接给一级菜单配menuAction
};
// 5. 把一级菜单放进菜单数组
const menuItems: Array<statusBarManager.StatusBarMenuItem> = [menuItem];
// 6. 把菜单数组放进菜单组(菜单组可以包含多个一级菜单,这里先放一个)
const groupMenus: Array<statusBarManager.StatusBarGroupMenu> = [menuItems];
return groupMenus;
}
小提醒:
menuCode要唯一,后面监听右键点击时,就是通过这个menuCode判断用户点了哪个菜单;- 一级菜单的
menuAction和subMenu不能都为空,不然菜单显示了但点击没反应,至少要有一个。
第七步:整合所有配置,调用 addToStatusBar 接入状态栏
前面咱们已经配置好了图标(statusBarIcon)、左键弹窗(QuickOperation)、右键菜单(groupMenus),现在要把它们整合到StatusBarItem里,然后调用addToStatusBar接口,把图标加到状态栏!
7.1 新增 addIconToStatusBar 函数
在index.ets的 Index 结构体里,新增这个核心函数:
// 整合所有配置,把图标添加到状态栏
private async addIconToStatusBar() {
// 1. 获取上下文(前面已经获取过,这里再检查一遍)
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法添加状态栏图标!');
promptAction.showToast({ message: '添加失败:上下文获取失败' });
return;
}
// 2. 检查图标配置是否存在(前面configStatusBarIcon函数里赋值的)
if (!this.statusBarIcon) {
console.error('图标配置不存在,先配置图标!');
promptAction.showToast({ message: '添加失败:图标未配置' });
// 如果没配置,先调用配置图标函数
await this.configStatusBarIcon();
// 配置后再检查一次,如果还是没有,就返回
if (!this.statusBarIcon) {
return;
}
}
// 3. 获取弹窗和菜单配置
const quickOperation = this.configQuickOperation();
const groupMenus = this.configRightMenu();
// 4. 整合所有配置到StatusBarItem
const statusBarItem: statusBarManager.StatusBarItem = {
icons: this.statusBarIcon, // 图标配置
quickOperation: quickOperation, // 左键弹窗配置
statusBarGroupMenu: groupMenus // 右键菜单配置(可选,不想要可以不写)
};
// 5. 调用addToStatusBar接口,添加到状态栏
try {
statusBarManager.addToStatusBar(context, statusBarItem);
console.info('图标成功添加到状态栏!');
promptAction.showToast({ message: '图标已添加到状态栏', duration: 2000 });
// 6. 注册点击监听(可选,根据需要加)
this.registerClickListeners();
} catch (error) {
const errorMsg = `添加失败:${(error as Error).message}`;
console.error(errorMsg);
promptAction.showToast({ message: errorMsg, duration: 2000 });
}
}
// 注册图标和菜单的点击监听(前面讲过的on接口)
private registerClickListeners() {
// 注册图标点击监听
statusBarManager.on('statusBarIconClick', this.onStatusBarIconClick);
// 注册右键菜单点击监听
statusBarManager.on('rightMenuClick', this.onRightMenuClick);
console.info('点击监听已注册~');
}
// 图标点击的回调函数(前面讲过的)
private onStatusBarIconClick = (eventData: emitter.EventData) => {
const data = eventData.data as { iconClickType: string };
if (data && data.iconClickType === 'leftClickType') {
console.info('用户左键点击了状态栏图标,弹窗会自动弹出~');
// 因为咱们配置了quickOperation.abilityName,所以这里不用手动处理弹窗,系统会自动加载
}
};
// 右键菜单点击的回调函数(前面讲过的)
private onRightMenuClick = (eventData: emitter.EventData) => {
const data = eventData.data as { menuCode: string };
if (!data) return;
const menuCode = data.menuCode;
console.info(`用户点击了右键菜单:${menuCode}`);
// 根据menuCode处理业务
switch (menuCode) {
case 'openHome':
// 打开主页(如果当前就在主页,不用跳转)
if (router.getCurrentUrl() !== 'pages/Index') {
router.pushUrl({ url: 'pages/Index' });
promptAction.showToast({ message: '正在打开主页~' });
} else {
promptAction.showToast({ message: '已经在主页啦~' });
}
break;
case 'showDetail':
// 打开详情页(假设详情页路径是pages/Detail)
router.pushUrl({ url: 'pages/Detail' });
promptAction.showToast({ message: '正在打开详情页~' });
break;
default:
promptAction.showToast({ message: `未知菜单:${menuCode}` });
break;
}
};
7.2 绑定按钮点击事件
咱们在build函数里有个 “添加图标到状态栏” 的按钮,现在把addIconToStatusBar函数绑定到按钮的onClick事件上(前面已经写了,这里再确认一下):
Button('添加图标到状态栏')
.width(250)
.height(50)
.fontSize(18)
.backgroundColor('#007AFF')
.onClick(() => {
this.addIconToStatusBar(); // 点击按钮调用添加函数
});
四、进阶操作:这些功能你可能也需要
咱们已经实现了基础的状态栏接入,但实际开发中,可能还需要动态更新图标、菜单,或者主动移除图标,这些进阶操作咱们也讲一讲,都是前面接口的实际应用。
1. 动态更新状态栏图标
比如音乐 APP 在播放时显示 “播放中” 图标,暂停时显示 “已暂停” 图标,代码如下(在index.ets里新增函数):
// 动态更新状态栏图标(参数是新图标的文件名,比如“playWhite.png”)
private async updateStatusBarIcon(newWhiteIconName: string, newBlackIconName: string) {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法更新图标!');
return;
}
try {
const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
// 读取新的白色图标
const newWhiteFileData = resourceMgr.getRawFileContentSync(newWhiteIconName);
const newWhiteBuffer = newWhiteFileData.buffer;
const newWhiteImageSource = image.createImageSource(newWhiteBuffer);
const newWhitePixelMap = await newWhiteImageSource.createPixelMap();
// 读取新的黑色图标
const newBlackFileData = resourceMgr.getRawFileContentSync(newBlackIconName);
const newBlackBuffer = newBlackFileData.buffer;
const newBlackImageSource = image.createImageSource(newBlackBuffer);
const newBlackPixelMap = await newBlackImageSource.createPixelMap();
// 构建新的图标配置
const newIcon: statusBarManager.StatusBarIcon = {
white: newWhitePixelMap,
black: newBlackPixelMap
};
// 调用接口更新图标
statusBarManager.updateStatusBarIcon(context, newIcon);
console.info('状态栏图标更新成功!');
promptAction.showToast({ message: '图标已更新', duration: 2000 });
// 更新本地存储的图标配置
this.statusBarIcon = newIcon;
} catch (error) {
console.error(`更新图标失败:${(error as Error).message}`);
promptAction.showToast({ message: `更新图标失败:${(error as Error).message}` });
}
}
// 调用示例(比如音乐播放状态变化时)
private onMusicPlayStatusChange(isPlaying: boolean) {
if (isPlaying) {
// 播放中,更新为播放图标
this.updateStatusBarIcon('playWhite.png', 'playBlack.png');
} else {
// 已暂停,更新为暂停图标
this.updateStatusBarIcon('pauseWhite.png', 'pauseBlack.png');
}
}
2. 动态更新右键菜单
比如用户登录后,右键菜单显示 “个人中心”,未登录时显示 “登录”,代码如下:
// 根据登录状态更新右键菜单
private updateRightMenuByLoginStatus(isLoggedIn: boolean) {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法更新菜单!');
return;
}
let subMenus: Array<statusBarManager.StatusBarSubMenuItem> = [];
if (isLoggedIn) {
// 已登录,显示“个人中心”和“退出登录”
const subMenu1: statusBarManager.StatusBarSubMenuItem = {
subTitle: "个人中心",
menuCode: "userCenter",
menuAction: { abilityName: "UserCenterAbility", moduleName: "entry" }
};
const subMenu2: statusBarManager.StatusBarSubMenuItem = {
subTitle: "退出登录",
menuCode: "logout",
menuAction: { abilityName: "EntryAbility", moduleName: "entry" }
};
subMenus = [subMenu1, subMenu2];
} else {
// 未登录,显示“登录”
const subMenu: statusBarManager.StatusBarSubMenuItem = {
subTitle: "登录",
menuCode: "login",
menuAction: { abilityName: "LoginAbility", moduleName: "entry" }
};
subMenus = [subMenu];
}
// 构建一级菜单和菜单组
const menuItem: statusBarManager.StatusBarMenuItem = {
title: isLoggedIn ? "用户操作" : "请登录",
subMenu: subMenus
};
const menuItems: Array<statusBarManager.StatusBarMenuItem> = [menuItem];
const groupMenus: Array<statusBarManager.StatusBarGroupMenu> = [menuItems];
// 调用接口更新菜单
try {
statusBarManager.updateStatusBarMenu(context, groupMenus);
console.info(`右键菜单已更新为${isLoggedIn ? '登录后' : '未登录'}状态~`);
} catch (error) {
console.error(`更新菜单失败:${(error as Error).message}`);
}
}
// 调用示例(登录状态变化时)
this.onLoginStatusChange = (isLoggedIn: boolean) => {
this.updateRightMenuByLoginStatus(isLoggedIn);
};
3. 主动移除状态栏图标
比如用户点击 “退出服务” 按钮时,要把图标从状态栏移除,代码如下:
// 主动移除状态栏图标
private removeStatusBarIcon() {
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
console.error('获取上下文失败,没法移除图标!');
return;
}
try {
statusBarManager.removeFromStatusBar(context);
console.info('图标已从状态栏移除~');
promptAction.showToast({ message: '图标已移除', duration: 2000 });
// 移除图标后,注销点击监听
this.unregisterClickListeners();
} catch (error) {
console.error(`移除图标失败:${(error as Error).message}`);
promptAction.showToast({ message: `移除图标失败:${(error as Error).message}` });
}
}
// 注销所有点击监听
private unregisterClickListeners() {
statusBarManager.off('statusBarIconClick', this.onStatusBarIconClick);
statusBarManager.off('rightMenuClick', this.onRightMenuClick);
console.info('所有点击监听已注销~');
}
// 绑定到按钮点击事件
Button('移除状态栏图标')
.width(250)
.height(50)
.fontSize(18)
.backgroundColor('#FF3B30')
.onClick(() => {
this.removeStatusBarIcon();
});
五、踩坑指南!常见问题怎么解决?
咱们开发的时候,肯定会遇到各种问题,比如图标不显示、弹窗弹不出来、菜单点了没反应,我整理了几个常见问题,帮大家快速排查:
1. 状态栏不显示图标
可能原因 1:context 获取失败
- 排查:看日志里有没有 “getHostContext failed”,如果有,说明 context 没获取到;
- 解决:确保在 UI 页面中调用
this.getUIContext().getHostContext(),并且页面已经初始化完成(比如在onPageShow里调用,别在build里直接调用)。
可能原因 2:module.json5 配置错误
- 排查:检查
extensionAbilities里的name、type、srcEntry是否正确; - 解决:
name和 Ability 类名一致,type是statusBarView,srcEntry路径和实际文件路径一致。
可能原因 3:图片资源问题
- 排查:看日志里有没有 “读取图片失败”“createPixelMap 失败”;
- 解决:图片放在
rawfile里,文件名和代码一致,尺寸是 24vp*24vp,格式是 PNG。
2. 左键点击图标不弹弹窗
可能原因 1:QuickOperation 的 abilityName 错误
- 排查:检查
quickOperation.abilityName是否和module.json5里的name一致; - 解决:改成完全一致的名字,大小写都不能错。
可能原因 2:StatusBarPage 路径错误
- 排查:看
onSessionCreate里的session.loadContent路径是否正确; - 解决:比如页面在
pages/StatusBarPage,路径就写'pages/StatusBarPage',别多写或少写斜杠。
3. 右键菜单不显示或点击没反应
可能原因 1:statusBarGroupMenu 配置错误
- 排查:检查是否构建了 “菜单组→一级菜单→子菜单” 的结构,一级菜单的
subMenu或menuAction是否为空; - 解决:确保每个一级菜单至少有
subMenu或menuAction,menuCode唯一。
可能原因 2:没注册右键菜单点击监听
- 排查:看有没有调用
statusBarManager.on('rightMenuClick', ...); - 解决:在添加图标后,注册右键菜单监听。
4. 图标显示变形或模糊
可能原因:图片尺寸不是 24vp*24vp
- 排查:用图片查看工具检查图片尺寸;
- 解决:重新制作 24vp*24vp 的图片,别拉伸或压缩原有图片。
六、总结:整个流程再捋一遍
咱们今天从 “为啥做” 到 “怎么做”,把鸿蒙应用接入状态栏的整个流程讲透了,最后再帮大家捋一遍关键步骤,方便记忆:
- 备食材:导入需要的模块(桌面拓展、Ability、图片处理等);
- 搭架子:新建
StatusBarViewExtensionAbility(管理弹窗会话)和StatusBarPage(弹窗内容); - 上户口:在
module.json5里配置extensionAbilities,让系统识别 Ability; - 备图标:把 24vp*24vp 的黑白图标放进
rawfile; - 做配置:分别配置图标(
StatusBarIcon)、左键弹窗(QuickOperation)、右键菜单(statusBarGroupMenu); - 整合接入:把所有配置放进
StatusBarItem,调用addToStatusBar添加到状态栏; - 加功能:根据需要添加动态更新图标 / 菜单、主动移除图标、监听点击事件等进阶功能。
只要跟着这 7 步走,每个步骤仔细检查,别犯前面说的常见错误,你肯定能成功把应用接入状态栏!如果在开发中遇到其他问题,也可以去华为开发者官网(就是咱们参考的那个链接)看更详细的接口文档,或者在鸿蒙开发者社区提问,大家一起交流解决~
希望这篇文章能帮到各位小伙伴,祝大家在鸿蒙开发的路上越走越顺,开发出更多好用又好看的应用!
更多推荐



所有评论(0)