HarmonyOS鸿蒙Next开发者技术支持-悬浮工具箱案例实现方案
HarmonyOS鸿蒙Next开发者技术支持-悬浮工具箱案例实现方案
1. 问题说明:悬浮工具箱场景需求
1.1 问题场景
在移动应用中,用户经常需要在不同应用间快速切换常用工具。传统的工具入口需要返回桌面或切换应用,操作繁琐。悬浮工具箱可以在任意界面快速访问常用工具,提升操作效率。
1.2 具体表现
// 传统工具访问问题
interface ToolAccessIssues {
1: "需要退出当前应用才能使用工具";
2: "工具入口分散,查找困难";
3: "无法在特定场景快速调用";
4: "占用主屏幕空间";
5: "缺乏个性化定制";
}
1.3 实际应用场景
- 游戏过程中快速计算器
- 阅读时快速截图和标注
- 视频播放时亮度调节
- 多任务处理时快速笔记
- 系统设置一键调整
1.4 技术要求
- 支持悬浮窗显示和拖拽
- 基于zIndex确保悬浮层级
- 流畅的手势交互
- 可配置的工具项
- 自适应屏幕尺寸
2. 解决思路:整体架构设计
2.1 技术架构
基于HarmonyOS最新API设计的三层架构:
- UI层:使用ArkTS声明式UI,基于图片中的设计实现
- 业务层:工具管理、窗口控制、手势处理
- 系统层:窗口管理、权限控制、系统服务
2.2 核心API
[@ohos](/user/ohos).window:窗口管理API[@ohos](/user/ohos).gesture:手势识别API[@ohos](/user/ohos).zindex:层级管理API[@ohos](/user/ohos).preferences:数据持久化API
3. 解决方案:完整实现代码
3.1 配置权限和依赖
// module.json5 - 步骤1:配置应用权限
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
"reason": "需要显示悬浮窗功能",
"usedScene": {
"when": "always",
"abilities": ["EntryAbility"]
}
},
{
"name": "ohos.permission.CAPTURE_SCREEN",
"reason": "需要截图功能",
"usedScene": {
"when": "inuse",
"abilities": ["EntryAbility"]
}
}
],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:entryability_desc",
"icon": "$media:icon",
"label": "$string:entryability_label",
"startWindowIcon": "$media:float_icon",
"startWindowLabel": "悬浮工具箱"
}
]
}
}
此步骤配置应用所需的系统权限。SYSTEM_FLOAT_WINDOW权限允许应用创建悬浮窗,CAPTURE_SCREEN权限支持截图功能。同时定义了应用的入口Ability。
3.2 定义数据模型
// FloatModels.ets - 步骤2:定义数据模型和枚举
import { BusinessError } from '[@ohos](/user/ohos).base';
// 工具类型枚举 - 定义支持的工具类型
export enum ToolType {
SCREENSHOT = 1, // 截图工具
CALCULATOR = 2, // 计算器
BRIGHTNESS = 3, // 亮度调节
NOTE = 4, // 快速笔记
SETTINGS = 5, // 系统设置
CLIPBOARD = 6, // 剪贴板
CUSTOM = 99 // 自定义工具
}
// 悬浮窗位置配置接口
export interface FloatPosition {
x: number; // X坐标(相对于屏幕左上角)
y: number; // Y坐标(相对于屏幕左上角)
width: number; // 窗口宽度
height: number; // 窗口高度
}
// 工具配置接口
export interface ToolConfig {
id: string; // 工具唯一标识
name: string; // 工具显示名称
type: ToolType; // 工具类型
icon: Resource; // 图标资源
enabled: boolean; // 是否启用
order: number; // 显示顺序
description?: string; // 工具描述
}
// 悬浮窗状态接口
export interface FloatState {
isVisible: boolean; // 是否可见
position: FloatPosition; // 当前位置
opacity: number; // 透明度 0.0-1.0
isDragging: boolean; // 是否正在拖拽
currentTool?: ToolConfig; // 当前选中工具
}
// 进度信息接口(对应图片中的加载进度)
export interface ProgressInfo {
current: number; // 当前进度值
total: number; // 总进度值
description: string; // 进度描述
}
此步骤定义应用的核心数据模型。包括工具类型枚举、悬浮窗位置配置、工具配置、悬浮窗状态等接口。这些模型为后续的业务逻辑提供类型安全支持。
3.3 实现悬浮窗管理器
// FloatWindowManager.ets - 步骤3:实现悬浮窗核心管理器
import window from '[@ohos](/user/ohos).window';
import display from '[@ohos](/user/ohos).display';
import { BusinessError } from '[@ohos](/user/ohos).base';
/**
* 悬浮窗管理器 - 负责窗口的创建、显示、隐藏和位置管理
*/
export class FloatWindowManager {
private floatWindow: window.Window | null = null;
private screenInfo: display.Display | null = null;
private currentState: FloatState;
private dragStartPosition: { x: number, y: number } = { x: 0, y: 0 };
// 单例模式确保全局只有一个悬浮窗实例
private static instance: FloatWindowManager;
static getInstance(): FloatWindowManager {
if (!FloatWindowManager.instance) {
FloatWindowManager.instance = new FloatWindowManager();
}
return FloatWindowManager.instance;
}
constructor() {
this.currentState = {
isVisible: false,
position: { x: 0, y: 0, width: 160, height: 220 },
opacity: 0.9,
isDragging: false
};
}
// 步骤3.1:初始化显示信息
private async initDisplayInfo(): Promise<void> {
try {
this.screenInfo = await display.getDefaultDisplaySync();
console.info('屏幕信息获取成功:', JSON.stringify(this.screenInfo));
} catch (error) {
console.error('获取屏幕信息失败:', JSON.stringify(error));
throw error;
}
}
// 步骤3.2:创建悬浮窗
async createFloatWindow(context: common.BaseContext): Promise<void> {
try {
await this.initDisplayInfo();
// 使用最新的WindowStage创建API
const windowClass = window.WindowStage;
const windowStageContext = context as common.UIAbilityContext;
// 创建窗口实例
this.floatWindow = await windowClass.create(context, "float_toolbox");
// 设置窗口类型为悬浮窗
await this.floatWindow.setWindowType(window.WindowType.TYPE_FLOAT);
// 设置窗口属性 - 根据图片中的悬浮窗尺寸调整
const windowProperties: window.WindowProperties = {
windowRect: {
left: this.screenInfo!.width - 180, // 默认显示在右侧
top: Math.floor(this.screenInfo!.height / 2 - 110),
width: 160, // 对应图片中的宽度
height: 220 // 对应图片中的高度
},
isFullScreen: false,
isLayoutFullScreen: false,
focusable: true,
touchable: true,
isTransparent: true, // 支持透明背景
brightness: 1.0
};
await this.floatWindow.setWindowProperties(windowProperties);
// 设置窗口模式为悬浮
await this.floatWindow.setWindowMode(window.WindowMode.WINDOW_MODE_FLOATING);
// 设置背景透明
await this.floatWindow.setWindowBackgroundColor('#00000000');
// 更新当前状态
this.currentState.position = {
x: windowProperties.windowRect.left,
y: windowProperties.windowRect.top,
width: windowProperties.windowRect.width,
height: windowProperties.windowRect.height
};
this.currentState.isVisible = true;
console.info('悬浮窗创建成功');
} catch (error) {
console.error('创建悬浮窗失败:', JSON.stringify(error));
throw error;
}
}
// 步骤3.3:显示悬浮窗
async show(): Promise<void> {
if (!this.floatWindow) {
throw new Error('悬浮窗未创建');
}
try {
await this.floatWindow.show();
this.currentState.isVisible = true;
console.info('悬浮窗显示成功');
} catch (error) {
console.error('显示悬浮窗失败:', JSON.stringify(error));
throw error;
}
}
// 步骤3.4:隐藏悬浮窗
async hide(): Promise<void> {
if (!this.floatWindow) {
return;
}
try {
await this.floatWindow.hide();
this.currentState.isVisible = false;
console.info('悬浮窗隐藏成功');
} catch (error) {
console.error('隐藏悬浮窗失败:', JSON.stringify(error));
}
}
// 步骤3.5:更新窗口位置
async updatePosition(x: number, y: number): Promise<void> {
if (!this.floatWindow || !this.screenInfo) {
return;
}
try {
// 边界检查,确保窗口不会移出屏幕
const maxX = this.screenInfo.width - this.currentState.position.width;
const maxY = this.screenInfo.height - this.currentState.position.height;
const clampedX = Math.max(0, Math.min(x, maxX));
const clampedY = Math.max(0, Math.min(y, maxY));
await this.floatWindow.moveTo(clampedX, clampedY);
// 更新状态
this.currentState.position.x = clampedX;
this.currentState.position.y = clampedY;
console.info(`窗口位置更新到: (${clampedX}, ${clampedY})`);
} catch (error) {
console.error('更新窗口位置失败:', JSON.stringify(error));
}
}
// 步骤3.6:边缘吸附功能
private snapToEdge(x: number, y: number): { x: number, y: number } {
if (!this.screenInfo) return { x, y };
const SNAP_THRESHOLD = 50; // 吸附阈值50像素
const EDGE_MARGIN = 10; // 边缘边距
let newX = x;
let newY = y;
// 左侧吸附
if (x < SNAP_THRESHOLD) {
newX = EDGE_MARGIN;
}
// 右侧吸附
else if (x > this.screenInfo.width - this.currentState.position.width - SNAP_THRESHOLD) {
newX = this.screenInfo.width - this.currentState.position.width - EDGE_MARGIN;
}
// 顶部吸附
if (y < SNAP_THRESHOLD) {
newY = EDGE_MARGIN;
}
// 底部吸附
else if (y > this.screenInfo.height - this.currentState.position.height - SNAP_THRESHOLD) {
newY = this.screenInfo.height - this.currentState.position.height - EDGE_MARGIN;
}
return { x: newX, y: newY };
}
// 步骤3.7:手势处理 - 开始拖拽
onDragStart(x: number, y: number): void {
this.currentState.isDragging = true;
this.dragStartPosition = { x, y };
console.info(`开始拖拽,起点: (${x}, ${y})`);
}
// 步骤3.8:手势处理 - 拖拽移动
async onDragMove(x: number, y: number): Promise<void> {
if (!this.currentState.isDragging || !this.floatWindow) {
return;
}
// 计算相对位移
const deltaX = x - this.dragStartPosition.x;
const deltaY = y - this.dragStartPosition.y;
// 计算新位置
const newX = this.currentState.position.x + deltaX;
const newY = this.currentState.position.y + deltaY;
// 更新位置(拖拽过程中不进行边缘吸附)
await this.updatePosition(newX, newY);
// 更新起点位置
this.dragStartPosition = { x, y };
}
// 步骤3.9:手势处理 - 结束拖拽
async onDragEnd(x: number, y: number): Promise<void> {
if (!this.currentState.isDragging) {
return;
}
this.currentState.isDragging = false;
// 计算最终位置并进行边缘吸附
const deltaX = x - this.dragStartPosition.x;
const deltaY = y - this.dragStartPosition.y;
const finalX = this.currentState.position.x + deltaX;
const finalY = this.currentState.position.y + deltaY;
// 应用边缘吸附
const snappedPosition = this.snapToEdge(finalX, finalY);
// 更新到吸附后的位置
await this.updatePosition(snappedPosition.x, snappedPosition.y);
console.info(`拖拽结束,吸附到: (${snappedPosition.x}, ${snappedPosition.y})`);
}
// 步骤3.10:更新窗口透明度
async updateOpacity(opacity: number): Promise<void> {
if (!this.floatWindow) {
return;
}
try {
// 将透明度转换为16进制颜色值
const alpha = Math.round(opacity * 255);
const hexAlpha = alpha.toString(16).padStart(2, '0');
await this.floatWindow.setWindowBackgroundColor(`#${hexAlpha}000000`);
this.currentState.opacity = opacity;
console.info(`窗口透明度更新为: ${opacity}`);
} catch (error) {
console.error('更新透明度失败:', JSON.stringify(error));
}
}
// 步骤3.11:销毁窗口
async destroy(): Promise<void> {
if (!this.floatWindow) {
return;
}
try {
await this.floatWindow.destroy();
this.floatWindow = null;
this.currentState.isVisible = false;
console.info('悬浮窗销毁成功');
} catch (error) {
console.error('销毁悬浮窗失败:', JSON.stringify(error));
}
}
// 步骤3.12:获取当前状态
getCurrentState(): FloatState {
return { ...this.currentState };
}
// 步骤3.13:检查悬浮窗权限
async checkFloatPermission(): Promise<boolean> {
try {
const abilityAccessCtrl = abilityAccessCtrl.createAtManager();
const result = await abilityAccessCtrl.checkAccessToken(
abilityAccessCtrl.AssetType.ASSET_SYSTEM,
'ohos.permission.SYSTEM_FLOAT_WINDOW'
);
return result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
console.error('检查权限失败:', JSON.stringify(error));
return false;
}
}
}
此步骤实现悬浮窗的核心管理功能。包括窗口创建、显示/隐藏、位置管理、边缘吸附、手势处理等。使用单例模式确保全局只有一个悬浮窗实例,通过边界检查和边缘吸附确保良好的用户体验。
3.4 实现工具箱管理器
// ToolManager.ets - 步骤4:实现工具箱管理
import { preferences } from '[@kit](/user/kit).ArkData';
import { BusinessError } from '[@ohos](/user/ohos).base';
/**
* 工具箱管理器 - 负责工具的配置、存储和管理
*/
export class ToolManager {
private static readonly PREFERENCES_NAME = 'float_toolbox_config';
private static readonly KEY_TOOL_LIST = 'tool_list';
private static readonly KEY_WINDOW_STATE = 'window_state';
private static readonly KEY_PROGRESS = 'progress_info';
private preferences: preferences.Preferences | null = null;
private tools: ToolConfig[] = [];
// 默认工具配置 - 对应图片中的工具
private defaultTools: ToolConfig[] = [
{
id: 'tool_light',
name: '亮度',
type: ToolType.BRIGHTNESS,
icon: $r('app.media.ic_light'),
enabled: true,
order: 1,
description: '快速调节屏幕亮度'
},
{
id: 'tool_list',
name: '工具',
type: ToolType.SETTINGS,
icon: $r('app.media.ic_list'),
enabled: true,
order: 2,
description: '工具列表管理'
},
{
id: 'tool_calc',
name: '计算',
type: ToolType.CALCULATOR,
icon: $r('app.media.ic_calculator'),
enabled: true,
order: 3,
description: '快速计算器'
},
{
id: 'tool_more',
name: '更多',
type: ToolType.CUSTOM,
icon: $r('app.media.ic_more'),
enabled: true,
order: 4,
description: '查看更多工具'
}
];
// 步骤4.1:初始化管理器
async initialize(context: common.Context): Promise<void> {
try {
this.preferences = await preferences.getPreferences(context, {
name: ToolManager.PREFERENCES_NAME
});
// 加载工具配置
await this.loadToolConfig();
console.info('工具箱管理器初始化成功');
} catch (error) {
console.error('工具箱管理器初始化失败:', JSON.stringify(error));
throw error;
}
更多关于HarmonyOS鸿蒙Next开发者技术支持-悬浮工具箱案例实现方案的实战教程也可以访问 https://www.itying.com/category-93-b0.html
鸿蒙Next悬浮工具箱案例
基于ArkTS开发,使用WindowStage的loadContent方式创建悬浮窗,通过UIAbilityContext的createSubWindow接口管理子窗口。
关键实现包括:
- 使用
@ohos.window获取窗口实例并设置悬浮属性; - 通过
@ohos.multimodalInput监听触摸事件实现拖拽; - 利用
@ohos.display获取屏幕信息进行边界检测; - 使用自定义组件构建工具箱UI界面。
需在module.json5中配置悬浮窗权限与窗口属性。
更多关于HarmonyOS鸿蒙Next开发者技术支持-悬浮工具箱案例实现方案的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
这是一个非常专业和完整的HarmonyOS Next悬浮工具箱实现方案。您提供的代码结构清晰,架构设计合理,严格遵循了ArkTS声明式UI的开发范式,并充分利用了HarmonyOS的核心API。以下是对您方案的点评和几点关键补充:
方案亮点:
- 架构分层明确:UI层、业务层、系统层的三层架构设计合理,符合HarmonyOS应用开发的最佳实践。
- API使用准确:正确使用了
@ohos.window进行悬浮窗生命周期管理、@ohos.gesture处理拖拽手势、@kit.ArkData中的preferences进行数据持久化,这是实现该功能的核心。 - 用户体验细节到位:实现了窗口边缘吸附、拖拽过程中的实时反馈、透明度调节以及状态持久化(保存窗口位置),这些细节大大提升了工具的可用性。
- 代码质量高:采用了单例模式管理全局窗口实例,定义了完整的数据模型和接口,并进行了充分的错误捕获与日志记录,工程化程度很好。
关键点与优化建议:
-
@ohos.zindexAPI 的澄清:在您的核心API列表中提到了@ohos.zindex。在HarmonyOS中,窗口的层级管理主要通过window.Window的setWindowProperties或moveTo等接口实现,系统会管理窗口的显示次序。UI组件层内的层级则通过Stack容器及其子组件的顺序来控制。目前并没有一个独立的@ohos.zindex模块。您的实现中通过Stack和窗口管理API来控层是正确的做法。 -
FloatWindowManager中的窗口创建:// 注意:代码中这行可能需要调整 this.floatWindow = await windowClass.create(context, "float_toolbox");在HarmonyOS Next的Stage模型中,创建窗口通常与UIAbility的
WindowStage关联。对于应用内悬浮窗,更常见的做法是在UIAbility的onWindowStageCreate生命周期中,通过windowStage.getMainWindow()获得主窗口后,再使用window.createAPI创建子窗口(悬浮窗)。您代码中直接使用window.WindowStage.create的方式需要确认其上下文和场景的适用性。确保传入的context具有创建窗口的权限和能力上下文。 -
权限请求时机:
SYSTEM_FLOAT_WINDOW是敏感权限。除了在module.json5中声明,必须在运行时动态向用户申请。您的checkFloatPermission方法检查了权限状态,但通常需要配合abilityAccessCtrl.requestPermissionsFromUser在尝试创建悬浮窗前主动触发授权弹窗,以符合隐私规范。 -
FloatWindowComponent的集成:您定义了独立的悬浮窗UI组件,这很棒。需要确保在FloatWindowManager创建窗口后,能将此组件设置为该窗口的内容。这通常涉及在窗口创建后,调用窗口相关的loadContent方法,并指定该组件的ETS页面路径。 -
资源引用方式:在
ToolManager的defaultTools配置和UI构建中,使用了$r('app.media.xxx')引用资源。请确保项目resources/base/media/目录下确实存在对应的图片资源(如ic_light.png,ic_list.png等),否则运行时会导致资源找不到错误。
总结: 您已经掌握了HarmonyOS Next实现系统级悬浮工具的核心技术栈。当前方案距离可运行仅差几步之遥:确保窗口创建方式与Stage模型匹配、补充运行时权限申请逻辑、将悬浮窗UI组件正确加载到窗口中。 这些调整完成后,该案例将成为一个高质量、可复用的参考实现。

