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设计的三层架构:

  1. UI层:使用ArkTS声明式UI,基于图片中的设计实现
  2. 业务层:工具管理、窗口控制、手势处理
  3. 系统层:窗口管理、权限控制、系统服务

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

2 回复

鸿蒙Next悬浮工具箱案例

基于ArkTS开发,使用WindowStage的loadContent方式创建悬浮窗,通过UIAbilityContext的createSubWindow接口管理子窗口。

关键实现包括:

  1. 使用@ohos.window获取窗口实例并设置悬浮属性;
  2. 通过@ohos.multimodalInput监听触摸事件实现拖拽;
  3. 利用@ohos.display获取屏幕信息进行边界检测;
  4. 使用自定义组件构建工具箱UI界面。

需在module.json5中配置悬浮窗权限与窗口属性。

更多关于HarmonyOS鸿蒙Next开发者技术支持-悬浮工具箱案例实现方案的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这是一个非常专业和完整的HarmonyOS Next悬浮工具箱实现方案。您提供的代码结构清晰,架构设计合理,严格遵循了ArkTS声明式UI的开发范式,并充分利用了HarmonyOS的核心API。以下是对您方案的点评和几点关键补充:

方案亮点:

  1. 架构分层明确:UI层、业务层、系统层的三层架构设计合理,符合HarmonyOS应用开发的最佳实践。
  2. API使用准确:正确使用了@ohos.window进行悬浮窗生命周期管理、@ohos.gesture处理拖拽手势、@kit.ArkData中的preferences进行数据持久化,这是实现该功能的核心。
  3. 用户体验细节到位:实现了窗口边缘吸附、拖拽过程中的实时反馈、透明度调节以及状态持久化(保存窗口位置),这些细节大大提升了工具的可用性。
  4. 代码质量高:采用了单例模式管理全局窗口实例,定义了完整的数据模型和接口,并进行了充分的错误捕获与日志记录,工程化程度很好。

关键点与优化建议:

  1. @ohos.zindex API 的澄清:在您的核心API列表中提到了@ohos.zindex。在HarmonyOS中,窗口的层级管理主要通过window.WindowsetWindowPropertiesmoveTo等接口实现,系统会管理窗口的显示次序。UI组件层内的层级则通过Stack容器及其子组件的顺序来控制。目前并没有一个独立的@ohos.zindex模块。您的实现中通过Stack和窗口管理API来控层是正确的做法。

  2. FloatWindowManager 中的窗口创建

    // 注意:代码中这行可能需要调整
    this.floatWindow = await windowClass.create(context, "float_toolbox");
    

    在HarmonyOS Next的Stage模型中,创建窗口通常与UIAbility的WindowStage关联。对于应用内悬浮窗,更常见的做法是在UIAbility的onWindowStageCreate生命周期中,通过windowStage.getMainWindow()获得主窗口后,再使用window.create API创建子窗口(悬浮窗)。您代码中直接使用window.WindowStage.create的方式需要确认其上下文和场景的适用性。确保传入的context具有创建窗口的权限和能力上下文。

  3. 权限请求时机SYSTEM_FLOAT_WINDOW是敏感权限。除了在module.json5中声明,必须在运行时动态向用户申请。您的checkFloatPermission方法检查了权限状态,但通常需要配合abilityAccessCtrl.requestPermissionsFromUser在尝试创建悬浮窗前主动触发授权弹窗,以符合隐私规范。

  4. FloatWindowComponent 的集成:您定义了独立的悬浮窗UI组件,这很棒。需要确保在FloatWindowManager创建窗口后,能将此组件设置为该窗口的内容。这通常涉及在窗口创建后,调用窗口相关的loadContent方法,并指定该组件的ETS页面路径。

  5. 资源引用方式:在ToolManagerdefaultTools配置和UI构建中,使用了$r('app.media.xxx')引用资源。请确保项目resources/base/media/目录下确实存在对应的图片资源(如ic_light.png, ic_list.png等),否则运行时会导致资源找不到错误。

总结: 您已经掌握了HarmonyOS Next实现系统级悬浮工具的核心技术栈。当前方案距离可运行仅差几步之遥:确保窗口创建方式与Stage模型匹配、补充运行时权限申请逻辑、将悬浮窗UI组件正确加载到窗口中。 这些调整完成后,该案例将成为一个高质量、可复用的参考实现。

回到顶部