HarmonyOS鸿蒙Next中应用如何实现全局可以在手机桌面的悬浮按钮

HarmonyOS鸿蒙Next中应用如何实现全局可以在手机桌面的悬浮按钮 比如我的应用有个按钮,点击按钮显示悬浮球,同时当前应用进入后台,点击悬浮球app进入前台,同时悬浮球隐藏,悬浮球的布局可自定义,同时可以自由拖动,有什么方式实现

4 回复

开发者你好,全局悬浮按钮可以参考下以下方案:

方案 优缺点 适用场景
方案一:通过系统提供的悬浮窗实现。 需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,多人视频通话、屏幕共享特殊场景与功能可申请该权限。 该方案适用于应用全局悬浮窗,应用进入后台,悬浮窗显示在桌面上,如多人视频通话、屏幕共享。
方案二:通过多UIAbility实现。 需申请ohos.permission.WINDOW_TOPMOST权限,目前仅PC/2in1支持。 该方案适用于应用全局悬浮窗,应用进入后台,悬浮窗显示在桌面上。
方案三:通过闪控球实现。 需申请ohos.permission.USE_FLOAT_BALL权限,特殊场景与功能可申请该权限,且需要API20及以上才可使用。 该方案适用于应用全局悬浮窗,如跨应用的题目搜索、账单记录、商品比价、翻译等。

方案二:通过系统提供的悬浮窗实现,通过createWindow的创建类型为TYPE_FLOAT的窗口即可实现。

  1. 在EntryAbility.ets中,通过AppStorage保存UIAbilityContext,具体实现如下:

    import { UIAbility } from '@kit.AbilityKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { window } from '@kit.ArkUI';
    
    const DOMAIN = 0x0000;
    
    export default class EntryAbility extends UIAbility {
      onDestroy(): void {
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
      }
    
      onWindowStageCreate(windowStage: window.WindowStage): void {
        // Main window is created, set main page for this ability
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
        windowStage.loadContent('pages/FloatWindowBySystemApi', (err) => {
          if (err.code) {
            hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
            return;
          }
          AppStorage.setOrCreate('uiContext', windowStage.getMainWindowSync()
            .getUIContext());
          AppStorage.setOrCreate('windowStage', windowStage);
          let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口
          // 2. 获取布局避让遮挡的区域
          let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR; // 以导航条避让为例
          let avoidArea = windowClass.getWindowAvoidArea(type);
          let bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航条区域的高度
          AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);
    
          type = window.AvoidAreaType.TYPE_SYSTEM; // 以状态栏避让为例
          avoidArea = windowClass.getWindowAvoidArea(type);
          let topRectHeight = avoidArea.topRect.height; // 获取状态栏区域高度
          AppStorage.setOrCreate('topRectHeight', topRectHeight);
          AppStorage.setOrCreate('context1', this.context);
          hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
        });
      }
    
      onWindowStageDestroy(): void {
        // Main window is destroyed, release UI related resources
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
      }
    
      onForeground(): void {
        // Ability has brought to foreground
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
      }
    
      onBackground(): void {
        // Ability has back to background
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
      }
    };
    
  2. 新建FloatWindowBySystemApi.ets页面,通过setUIContent、moveWindowTo、resize、showWindow方法分别设置窗口加载的内容页面、位置、大小、窗口的显示,具体实现如下:

    import { window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct FloatWindowBySystemApi {
      private context1: Context | undefined = AppStorage.get('context1');
      private windowClass: window.Window | null = null;
    
      build() {
        Column() {
          Text('打开悬浮窗')
            .fontSize(25)
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
              // 1.创建悬浮窗。
              let config: window.Configuration = {
                name: 'floatWindow', windowType: window.WindowType.TYPE_FLOAT, ctx: this.context1
              };
              window.createWindow(config, (err: BusinessError, data) => {
                let errCode: number = err.code;
                if (errCode) {
                  return;
                }
                this.windowClass = data;
                // 2.悬浮窗窗口创建成功后,设置悬浮窗的位置、大小及相关属性等。
                this.windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
                  let errCode: number = err.code;
                  if (errCode) {
                    return;
                  }
                });
                this.windowClass.resize(500, 500, (err: BusinessError) => {
                  let errCode: number = err.code;
                  if (errCode) {
                    return;
                  }
                });
                // 3.为悬浮窗加载对应的目标页面。
                this.windowClass.setUIContent('pages/Index', (err: BusinessError) => {
                  let errCode: number = err.code;
                  if (errCode) {
                    return;
                  }
                  // 3.显示悬浮窗。
                  (this.windowClass as window.Window).showWindow((err: BusinessError) => {
                    let errCode: number = err.code;
                    if (errCode) {
                      return;
                    }
                  });
                });
              });
            });
          Text('关闭悬浮窗')
            .fontSize(25)
            .margin({ top: 15 })
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
              this.windowClass?.destroyWindow();
            });
        }
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .height('100%')
        .width('100%');
      }
    }
    
  3. 新建悬浮窗内容页面Index.ets,具体实现如下:

    import { display, window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct Index {
      private windowX: number = 100;
      private windowY: number = 100;
      private floatWindow: window.Window | null = null;
      private screenWidth: number = 0;
    
      aboutToAppear() {
        // 获取悬浮窗实例
        this.floatWindow = window.findWindow('floatWindow');
        // 获取屏幕尺寸
        let displayInfo = display.getDefaultDisplaySync();
        this.screenWidth = displayInfo.width;
      }
    
      // 处理拖拽更新
      private onPan(event: GestureEvent) {
        this.windowX += event.offsetX;
        this.windowY += event.offsetY;
        if (this.floatWindow) {
          this.floatWindow.moveWindowTo(this.windowX, this.windowY);
        }
      }
    
      // 处理拖拽结束,实现自动贴边
      private onPanEnd() {
        const midX = this.screenWidth / 2;
        const threshold = 100; // 贴边阈值,可根据需要调整
        let targetX = this.windowX;
        // 判断是否靠近屏幕中间
        if (Math.abs(this.windowX - midX) < threshold) {
          targetX = this.windowX > midX ? this.screenWidth - 200 : 0; // 200为窗口宽度
        }
        // 更新窗口位置
        if (this.floatWindow) {
          this.floatWindow.moveWindowTo(targetX, this.windowY);
        }
      }
    
      build() {
        Row() {
          Column() {
            // 真实项目中可替换为开发者真实的图片
            Image($r('app.media.play'))
              .margin({ left: 10, top: 10 })
              .width(55)
              .height(55);
            Text('ArkTS是HarmonyOS主力应用开发语言。')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .margin({ left: 10, top: 10 });
          }
          .width('100%');
        }
        .gesture(
          PanGesture({ direction: PanDirection.All })
            .onActionUpdate((event: GestureEvent) => {
              this.onPan(event);
            })
            .onActionEnd(() => {
              this.onPanEnd();
            })
        )
        .height('100%');
      }
    }
    
  4. 在module.json5中添加requestPermissions并新增ohos.permission.SYSTEM_FLOAT_WINDOW权限。

    {
      "name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
    },
    

方案二:通过多Ability实现,该方案主要通过startAbility拉起UIAbility,并隐藏窗口的标题栏和控制窗口大小和层级来实现。

  1. 新建一个名称为的FloatWindowAbility的UIAbility,通过setWindowDecorVisible隐藏窗口的标题栏,通过setWindowTitleButtonVisible隐藏标题栏上的最大化、最小化、关闭按钮,再通过setWindowTopmost方法设置窗口置顶,具体实现如下:

    import { UIAbility } from '@kit.AbilityKit';
    import { window } from '@kit.ArkUI';
    
    export default class FloatWindowAbility extends UIAbility {
      private async resizeWindow(win: window.Window, width: number, height: number): Promise<void> {
        try {
          await win.resize(width, height);
          console.info('Succeeded in resizing window.');
        } catch (err) {
          console.error(`resize window failed: <${err.code}>${err.message}`);
        }
      }
    
      private setDecorVisible(win: window.Window, isVisible: boolean) {
        try {
          win.setWindowDecorVisible(isVisible);
          console.info('set window decor visible success.');
        } catch (err) {
          console.error(`set window decor visible failed: <${err.code}>${err.message}`);
        }
      }
    
      private setTitleButtonVisible(win: window.Window, isMaxVisible: boolean,
        isMinVisible: boolean, isCloseVisible?: boolean) {
        try {
          win.setWindowTitleButtonVisible(isMaxVisible, isMinVisible, isCloseVisible);
          console.info('set window title button visible success.');
        } catch (err) {
          console.error(`set window title button visible failed: <${err.code}>${err.message}`);
        }
      }
    
      private async setTopmost(win: window.Window, isWindowTopmost: boolean): Promise<void> {
        try {
          await win.setWindowTopmost(isWindowTopmost);
          console.info('set window topmost success.');
        } catch (err) {
          console.error(`set window topmost failed: <${err.code}>${err.message}`);
        }
      }
    
      onWindowStageCreate(windowStage: window.WindowStage): void {
        // 加载主窗口对应的页面。
        windowStage.loadContent('pages/FloatWindowByAbilityDetailPage', () => {
          let mainWindow: window.Window | undefined = undefined;
          // 获取应用主窗口。
          windowStage.getMainWindow().then(async (data: window.Window) => {
            if (!data) {
              return;
            }
            mainWindow = data;
            // 设置窗口大小
            await this.resizeWindow(mainWindow, 300, 400);
            // 设置窗口标题可见
            this.setDecorVisible(mainWindow, false);
            // 设置窗口最大化、最小化、关闭按钮可见
            this.setTitleButtonVisible(mainWindow, false, false, false);
            // 设置窗口置顶
            await this.setTopmost(mainWindow, true);
          }).catch((err: BusinessError) => {
            if (err.code) {
              console.error(`Failed to obtain the main window. Cause code: ${err.code}, message: ${err.message}`);
            }
          });
        });
      }
    };
    
  2. 新建FloatWindowByAbility.ets页面,通过startAbility启动第一步创建的FloatWindowAbility,具体实现如下:

    import { common, Want } from '@kit.AbilityKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    
    @Entry
    @Component
    struct FloatWindowByAbility {
      private abilityContext: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
    
      build() {
        Column({ space: 20 }) {
          Button('拉起悬浮窗')
            .onClick(() => {
              let want: Want = {
                // 此处需要根据实际包名进行更改
                bundleName: 'com.example.floatwindowdemo',
                abilityName: 'FloatWindowAbility',
                moduleName: 'entry',
              };
              this.abilityContext?.startAbility(want)
                .then(() => {
                  console.info('start ability success');
                }).catch((error: BusinessError) => {
                console.error(`start ability failed, code: ${error.code}, message: ${error.message}`);
              });
            });
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center);
      }
    }
    
  3. 新建FloatWindowByAbilityDetailPage.ets,用来承载悬浮窗的内容,具体实现如下:

    @Entry
    @Component
    struct FloatWindowByAbilityDetailPage {
      build() {
        Row() {
          Column() {
            Image($r('app.media.play'))
              .margin({ left: 10, top: 10 })
              .width(55)
              .height(55);
            Text('ArkTS是HarmonyOS主力应用开发语言,基于TypeScript(简称TS)语言扩展而来,是TS的超集。')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .margin({ left: 10, top: 10 });
          }
          .width('100%');
        }
        .height('100%')
        .width('100%');
      }
    }
    
  4. 在module.json5中添加requestPermissions并新增ohos.permission.WINDOW_TOPMOST权限。

    {
      "name": "ohos.permission.WINDOW_TOPMOST"
    },
    

方案三:通过闪控球实现。 闪控球是一种在设备屏幕上悬浮的非全屏应用窗口,为应用提供临时的全局能力,完成跨应用交互,基于安全考虑,仅允许应用在前台时启动闪控球,并且需要具有ohos.permission.USE_FLOAT_BALL权限,仅支持手机和平板设备。同一个应用只能启动一个闪控球,同一个设备最多同时存在两个闪控球,在超出闪控球最大个数限制时,打开新的闪控球会替换最早启动的闪控球。 开发步骤: 1.导入模块并声明闪控球控制器。 2.使用creat()接口创建闪控球控制器实例后注册点击事件回调和状态变化事件回调,通过startFloatingBall()接口启动闪控球。 3.通过updateFloatingBall()更新闪控球信息,以此控制闪控球展示的内容。 4.通过stopFloatingBall()停止闪控球。当不再需要显示闪控球时,可根据业务需要关闭闪控球。详细内容见demo参考闪控球开发步骤。 需要应用退到后台可以将当前应用窗口最小化:

// 获取Context
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
// 通过Context获取windowStage
let windowStage = context.windowStage;
// 通过windowStage获取主窗口
let mainWindow = windowStage.getMainWindowSync();
// 将主窗口最小化
mainWindow.minimize();

更多关于HarmonyOS鸿蒙Next中应用如何实现全局可以在手机桌面的悬浮按钮的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


全局闪控球开发指导:全局闪控球开发指导-窗口管理-ArkUI

画中画窗口: [@ohos.PiPWindow (画中画窗口)-窗口管理-ArkTS API-ArkUI](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-pipwindow)

两种方法都有限制。

在HarmonyOS Next中,应用可通过UIExtensionAbility机制实现全局悬浮按钮。具体使用WindowManager创建悬浮窗,并设置类型为TYPE_FLOAT。需在module.json5中声明ohos.permission.SYSTEM_FLOAT_WINDOW权限,并配置UIExtensionAbility的metadata。通过startAbilityForResult启动UIExtensionAbility,并在其onWindowStageCreate生命周期中加载悬浮窗UI。

在HarmonyOS Next中,实现全局悬浮按钮(悬浮球)的核心是使用窗口(Window)UIAbility能力。这通常涉及创建一个始终位于顶层的、可拖动的、无焦点的小窗口。以下是实现的关键步骤和方式:

1. 核心实现方式:使用 Window 创建悬浮窗

  • 创建悬浮窗口:在你的UIAbility(例如 EntryAbility)中,通过 windowManager.createWindow 创建一个类型为 WindowType.TYPE_FLOAT 的浮动窗口。这个窗口将独立于应用主窗口。
  • 设置窗口属性:为该窗口配置参数,确保其无焦点(focusable: false)、可触摸、且位于所有应用之上(通过设置合适的 z-index 或窗口层级)。
  • 加载自定义布局:将你的悬浮球自定义UI(例如一个 ButtonImage 组件)加载到此窗口中。

2. 关键能力与权限

  • 权限声明:在 module.json5 配置文件中,需要申请悬浮窗权限:
    "requestPermissions": [
      {
        "name": "ohos.permission.SYSTEM_FLOAT_WINDOW"
      }
    ]
    
  • 动态权限申请:在运行时,首次创建前需通过 abilityAccessCtrl 动态请求用户授权。

3. 实现拖动与交互逻辑

  • 拖动实现:在悬浮球组件上绑定触摸事件(如 onTouch)。通过计算手指移动的偏移量,动态更新窗口的位置(使用 window.moveWindowTo 或直接更新组件位置)。
  • 点击隐藏与前台切换
    • 点击悬浮球:在悬浮球的点击事件中,调用 windowManager.destroyWindow 销毁悬浮窗口,同时通过 UIAbilityContextstartAbilityrestoreWindowStage 将应用的主UIAbility唤醒至前台。
    • 应用退后台时显示:在主UIAbility的 onBackground 生命周期回调中,创建并显示悬浮窗口。
    • 应用回前台时隐藏:在主UIAbility的 onForeground 生命周期回调中,销毁悬浮窗口。

4. 状态管理与通信

  • 由于悬浮窗口与主应用窗口属于不同的Window,它们之间的状态同步(如是否显示)需要通过公共状态管理(如使用 AppStorageEmitter 事件通信)来协调。

5. 注意事项

  • 系统限制:悬浮窗的创建和管理受系统管控,需遵循HarmonyOS的窗口管理规范,避免过度干扰用户。
  • 性能与功耗:悬浮窗应保持轻量,避免频繁刷新或高耗能操作,以节省电量。
  • 用户体验:拖动应流畅,点击响应需及时。建议提供设置选项,允许用户关闭此功能。

简要代码示例(概念性)

// 在UIAbility中创建悬浮窗
import windowManager from '@ohos.window';

let floatWindow: windowManager.Window | null = null;

async function createFloatWindow(context: common.UIAbilityContext) {
  let windowInfo: windowManager.WindowOptions = {
    name: 'floatBall',
    windowType: windowManager.WindowType.TYPE_FLOAT,
    ctx: context,
    // 其他配置:尺寸、位置等
  };
  floatWindow = await windowManager.createWindow(context, windowInfo);
  // 加载自定义悬浮球UI
  floatWindow.loadContent('pages/FloatBall', (err, data) => {});
  await floatWindow.show();
}

// 拖动示例(在悬浮球页面中)
@Entry
@Component
struct FloatBall {
  @State offsetX: number = 0;
  @State offsetY: number = 0;

  build() {
    Button()
      .onTouch((event: TouchEvent) => {
        // 计算并更新窗口位置
      })
      .onClick(() => {
        // 1. 销毁悬浮窗
        // 2. 唤醒主应用至前台
      })
  }
}

总结:通过 Window 能力创建浮动窗口是实现该功能的标准方法。重点在于窗口的生命周期管理与主UIAbility的协同,以及流畅的拖动交互实现。

回到顶部