HarmonyOS鸿蒙Next中怎么实现全局window悬浮框

HarmonyOS鸿蒙Next中怎么实现全局window悬浮框 怎么实现全局的window悬浮框,可随意拖动,当拖动到中间时,会自动计算贴边显示

9 回复

【背景知识】

设置应用子窗口:开发者可以按需创建应用子窗口,如弹窗等,并对其进行属性设置等操作。

【解决方案】

1、在Index中定义主页面,并在主页面的aboutToAppear生命周期函数中加载主页面展示数据。

aboutToAppear(): void {
  // 模拟加载数据
  this.imageDetailList = ImageDetailViewModel.getDefaultImageDetailList();
  // 展示悬浮工具球
  this.showToolBall()
  // 全局保存滚动器
  AppStorage.setOrCreate('scroller', this.scroller)
  // 全局保存图库长度
  AppStorage.setOrCreate('length', this.imageDetailList.length)
}

2、在应用启动时显示悬浮工具球,自定义showToolBall函数,调用createSubWindow创建子窗口,并依次设置子窗口加载页、子窗口右下坐标、子窗口大小和展示子窗口。

 // 加载工具球子窗口
 showToolBall() {
   // 创建子窗口
   this.windowStage.createSubWindow(CommonConstants.ToolBall_SUBWINDOW, (err, windowClass) => {
     if (err.code > 0) {
       return;
     }
     try {
       // 设置子窗口加载页
       windowClass.setUIContent('subwindow/ToolBallSubWindow', () => {
         windowClass.setWindowBackgroundColor(CommonConstants.WINDOW_BACKGROUND_COLOR);
       });
       // 设置子窗口右下坐标
       windowClass.moveWindowTo(CommonConstants.INIT_SUBWINDOW_POSITION_X, CommonConstants.INIT_SUBWINDOW_POSITION_Y);
       // 设置子窗口大小
       windowClass.resize(vp2px(CommonConstants.INIT_SUBWINDOW_SIZE), vp2px(CommonConstants.INIT_SUBWINDOW_SIZE));
       // 展示子窗口
       windowClass.showWindow();
     } catch (err) {
     }
   })
 }

3、实现子窗口加载页,设计悬浮球展开态和非展开态两种展示效果。当悬浮球处于展开态时,根据当前悬浮球所在位置,判断展开方向,展示悬浮球中的多个工具项,并自定义各工具项点击效果,如页面上滑、页面下拉、退出当前应用等。当悬浮球处于非展开态时,利用gesture监听组件拖拽手势,实现悬浮球拖拽效果。

@Builder
commonUnExpand() {
}
.width(CommonConstants.INIT_SUBWINDOW_SIZE)
.height(CommonConstants.INIT_SUBWINDOW_SIZE)
.onClick(async () => {
  this.isExpand = true
  // 展开
  await switchFloatingWindowStyle(this.isExpand, this.subWindow);
})
.gesture(
  PanGesture(this.panOption) // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition
    // 拖拽时关闭扩展
    .onActionStart(async () => {
    })
    // 拖拽
    .onActionUpdate((event: GestureEvent) => {
      dragToMove(event, this.subWindow, this.windowPosition);
    })
    .onActionEnd(() => {
      // 吸顶后,判断展开方向
      this.dir = edgeDetermination(this.subWindow, this.windowPosition);
      ChangeFocus(this.windowStage, CommonConstants.ToolBall_SUBWINDOW);
    })
)

完整示例代码:应用内悬浮工具球

更多关于HarmonyOS鸿蒙Next中怎么实现全局window悬浮框的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


一、视频播放、直播、视频通话、视频会议等可以使用画中画

https://developer.huawei.com/consumer/cn/doc/design-guides/pip-0000001927422624

二、悬浮窗有限制,只能特定场景的2in1可以申请

创建WindowType.TYPE_FLOAT即悬浮窗类型的窗口,需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,该权限为受控开放权限,仅符合指定场景的在2in1设备上的应用可申请该权限。申请方式请参考:申请使用受限权限

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/application-window-stage#%E8%AE%BE%E7%BD%AE%E6%82%AC%E6%B5%AE%E7%AA%97%E5%8F%97%E9%99%90%E5%BC%80%E6%94%BE

三、自己以组件的方式实现窗口悬浮 拖拽,缩放

  1. 申请悬浮窗权限

module.json5中配置权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
        "reason": "需要悬浮窗权限",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

2. 创建悬浮窗并实现拖拽贴边

使用@kit.ArkUIwindow模块创建悬浮窗,并在UI组件中实现拖拽手势和自动贴边逻辑:

import { UIAbility } from '@kit.AbilityKit';
import { window, display } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 获取屏幕尺寸
    let displayInfo = display.getDefaultDisplaySync();
    let screenWidth = displayInfo.width;
    let screenHeight = displayInfo.height;

    // 创建悬浮窗配置
    let config: window.WindowConfig = {
      name: "globalFloatWindow",
      windowType: window.WindowType.TYPE_FLOAT,
      ctx: this.context // 使用Ability上下文
    };

    // 创建悬浮窗
    window.createWindow(config, (err, win) => {
      if (err) {
        console.error(`创建悬浮窗失败: code: ${err.code}, message: ${err.message}`);
        return;
      }
      let floatWindow = win;
      // 设置初始位置和大小
      floatWindow.moveWindowTo(100, 100);
      floatWindow.resize(200, 200);
      // 加载UI内容
      floatWindow.setUIContent("pages/FloatPage", (err) => {
        if (err) {
          console.error(`设置UI内容失败: code: ${err.code}, message: ${err.message}`);
          return;
        }
        floatWindow.showWindow();
      });
    });
  }
}

实现悬浮窗内容页面:

import { window, display } from '@kit.ArkUI';

@Entry
@Component
struct FloatPage {
  @State windowX: number = 100;
  @State windowY: number = 100;
  private floatWindow: window.Window | null = null;
  private screenWidth: number = 0;
  private screenHeight: number = 0;

  aboutToAppear() {
    // 获取悬浮窗实例
    this.floatWindow = window.findWindow("globalFloatWindow");
    // 获取屏幕尺寸
    let displayInfo = display.getDefaultDisplaySync();
    this.screenWidth = displayInfo.width;
    this.screenHeight = displayInfo.height;
  }

  // 处理拖拽更新
  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() {
    Column() {
      Text('悬浮窗内容')
        .fontSize(16)
        .backgroundColor(Color.White)
        .width('100%')
        .height('100%')
    }
    .width('100%')
    .height('100%')
    .gesture(
      PanGesture({ direction: PanDirection.All })
        .onActionUpdate((event: GestureEvent) => {
          this.onPan(event);
        })
        .onActionEnd(() => {
          this.onPanEnd();
        })
    )
  }
}

3. 销毁悬浮窗

当不需要悬浮窗时,调用销毁方法:

if (this.floatWindow) {
  this.floatWindow.destroyWindow((err) => {
    if (err) {
      console.error(`销毁悬浮窗失败: code: ${err.code}, message: ${err.message}`);
    }
  });
}

有要学HarmonyOS AI的同学吗,联系我:https://www.itying.com/goods-1206.html

楼主可以参考改造这个示例来做:代码仓库里面有完整的示例
应用内悬浮工具球-关键场景示例-实用工具类行业实践-行业实践 - 华为HarmonyOS开发者

previewableImage

实现流程

1/创建子窗口

// 在Ability的onWindowStageCreate中创建

onWindowStageCreate(windowStage: window.WindowStage) {

  windowStage.createSubWindow('floatWindow', (err, data) => {

    if (!err) {

      data.moveTo(0, 200) // 初始位置

      data.resize(200, 200) // 窗口尺寸

      data.loadContent('pages/FloatWindowPage') // 加载悬浮窗内容页

    }

  })

}

2/在悬浮窗内容页添加拖动手势:

// FloatWindowPage.ets

@Entry

@Component

struct FloatWindowPage {

  @State offsetX: number = 0

  @State offsetY: number = 0

  build() {

    Column() {

      // 悬浮窗内容组件

    }

    .width(200).height(200)

    .position({ x: this.offsetX, y: this.offsetY })

    .PanGesture()

    .onActionUpdate((event: GestureEvent) => {

      this.offsetX += event.offsetX

      this.offsetY += event.offsetY

    })

    .onActionEnd(() => {

      this.autoEdgeDetection() // 触发贴边计算

    })

  }

}

3/计算窗口与屏幕边界贴边距离

private autoEdgeDetection() {

  const screenWidth = 1080 // 通过display.getDefaultDisplaySync获取真实值

  const windowCenterX = this.offsetX + 100 // 窗口宽度200时中点为100

  // 计算最近边界

  if (windowCenterX > screenWidth / 2) {

    this.offsetX = screenWidth - 200 // 贴右边缘

  } else {

    this.offsetX = 0 // 贴左边缘

  }

  // 添加动画效果

  animateTo({

    duration: 300,

    curve: Curve.EaseOut

  }, () => {

    this.offsetX = this.offsetX

  })

}

具体实现代码可参考以下:
window悬浮框内代码:

import { display, inspector, window } from '@kit.ArkUI';
import { WindowAppStorage } from './WindowAppStorage';

export interface WindowPosition {
  x: number,
  y: number
}

@Entry
@Component
export struct WindowPage {
  @State windowStage: window.WindowStage = AppStorage.get('windowStage') as window.WindowStage;
  @State subWindow: window.Window = window.findWindow(WindowAppStorage.windowName);
  @State @Watch('moveWindow') windowPosition: WindowPosition = {
    x: 0,
    y: WindowAppStorage.windowY
  };
  // @State windowWidth:number = CommonConstants.window_default_width;
  // @State windowHeight:number = CommonConstants.window_default_height;
  listener: inspector.ComponentObserver = inspector.createComponentObserver('COMPONENT_ID');
  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
  private callback: () => void = () => {
    this.subWindow.resize(vp2px(WindowAppStorage.windowWidth), vp2px(WindowAppStorage.windowHeight));
  };
  @State displayWidth:number = 0
  @State displayHeight:number = 0
  @State floatWindowPagePadding: number = 8

  aboutToAppear(): void {
    this.displayWidth = px2vp(display.getDefaultDisplaySync().width)
    this.displayHeight = px2vp(display.getDefaultDisplaySync().height)
  }
  onPageShow(): void {
    setTimeout(() => {
      try {
        let subWindowID: number = window.findWindow('WindowPage').getWindowProperties().id;
        let mainWindowID: number = this.windowStage.getMainWindowSync().getWindowProperties().id;
        //通过window.shiftAppWindowFocus转移窗口焦点实现创建子窗口后,主窗口依然可以响应事件
        window.shiftAppWindowFocus(subWindowID, mainWindowID);
      } catch (error) {
        // LoggerUtils.error('shiftAppWindowFocus failed' + JSON.stringify(error));
      }
    }, 500)
    this.listener.on('layout', this.callback);
  }

  onPageHide(): void {
    this.listener.off('layout', this.callback);
  }

  destroyView() {
    this.subWindow.destroyWindow()
  }

  /**
   * Move the floating window to the specified position.
   */
  moveWindow() {
    this.subWindow.moveWindowTo(vp2px(this.windowPosition.x), vp2px(this.windowPosition.y));
  }

  build() {
    Row() {
      this.openBuilder()
    }
    .backgroundColor(Color.Transparent)
    .gesture(
      PanGesture(this.panOption)
        .onActionStart((event: GestureEvent) => {
        })
        .onActionUpdate((event: GestureEvent) => {
          let x:number = this.windowPosition.x
          let y:number = this.windowPosition.y
          x += px2vp(event.offsetX);
          y += px2vp(event.offsetY);
          let top = WindowAppStorage.windowTop;
          let bottom = this.displayHeight - WindowAppStorage.windowTop;
          if (y < top) {
            y = top;
          } else if (y > bottom) {
            y = bottom;
          }
          this.windowPosition = {
            x:x,
            y:y
          }
        })
        .onActionEnd((event: GestureEvent) => {
          let x:number = this.windowPosition.x
          let y:number = this.windowPosition.y
          if ((x + (px2vp(this.subWindow.getWindowProperties().windowRect.width) / 2)) >= (this.displayWidth / 2)) {
            x = this.displayWidth - px2vp(this.subWindow.getWindowProperties().windowRect.width) - this.floatWindowPagePadding;
          } else if (event.offsetX < (this.displayWidth / 2)) {
            x = this.floatWindowPagePadding;
          }
          let top = WindowAppStorage.windowTop;
          let bottom = this.displayHeight - 5;
          if (y < top) {
            y = top;
          } else if (y > bottom) {
            y = bottom;
          }
          this.windowPosition = {
            x:x,
            y:y
          }
        })
    )
  }

  @Builder
  openBuilder() {
    Row() {
       Row(){
         Row(){
           Text('哈哈')
             .fontColor(Color.White)
         }
         .justifyContent(FlexAlign.Center)
         .backgroundColor(Color.Blue)
         .width('100%')
         .height(WindowAppStorage.windowHeight)
         .layoutWeight(1)
         .onClick(()=>{
           })
         Button('关闭')
           .backgroundColor(Color.Gray)
           .height(WindowAppStorage.windowHeight)
           .padding(0)
           .onClick(()=>{
             this.destroyView()
           })
       }
      .height(WindowAppStorage.windowHeight)
      .width(WindowAppStorage.windowWidth)
    }
    .id('COMPONENT_ID')
    .height(WindowAppStorage.windowHeight)
  }
}

打开悬浮框代码:

 Button('打开window悬浮框')
          .height(80)
          .padding({left:12,right:12})
          .backgroundColor(Color.Red)
          .fontColor(Color.White)
          .onClick(()=>{
           if (this.isShowWindow()){
             this.removeWindow()
           }
            ShowWindow.show()
          })
        Button('关闭window悬浮框')
          .height(80)
          .padding({left:12,right:12})
          .backgroundColor(Color.Red)
          .fontColor(Color.White)
          .onClick(()=>{
            if (this.isShowWindow()){
              this.removeWindow()
            }
          })
//关闭悬浮框
  async removeWindow(){
    let subWindow: window.Window = window.findWindow(WindowAppStorage.windowName);
    await subWindow.destroyWindow()
  }

  ///该window是否在显示
  isShowWindow():boolean{
    try {
      let subWindow: window.Window = window.findWindow(WindowAppStorage.windowName);
      return subWindow.isWindowShowing()
    } catch (e) {
      return false
    }
    return false
  }
import { window } from '@kit.ArkUI';
import { WindowAppStorage } from './WindowAppStorage';

@Component
export default struct WindowComponent {

  show(){
    let windowStage: window.WindowStage = AppStorage.get('windowStage') as window.WindowStage;
    windowStage.createSubWindow(WindowAppStorage.windowName, (err, windowClass) => {
      if (err.code > 0) {
        debugger
        // LoggerUtils.error('failed to create subWindow Cause:' + err.message);
        return;
      }
      try {
        windowClass.setUIContent('pages/WindowPage', () => {
          windowClass.setWindowBackgroundColor('#00000000');
        });
        windowClass.resize(vp2px(WindowAppStorage.windowWidth), vp2px(WindowAppStorage.windowHeight));
        windowClass.moveWindowTo(0, vp2px(WindowAppStorage.windowY));
        windowClass.showWindow();
        windowClass.setWindowLayoutFullScreen(true);
      } catch (err) {
        debugger
        // LoggerUtils.error('failed to create subWindow Cause:' + err);
      }
    })
  }
  build() {

  }
}
export class  ShowWindow {
  static show(): void{
    return new WindowComponent().show()
  }
}

在HarmonyOS Next中,可通过WindowManager模块创建全局悬浮窗。使用window.createWindow方法并指定WindowType.TYPE_FLOAT类型,设置布局参数与视图。通过windowManager.moveWindowTo调整位置,windowManager.showWindow显示悬浮窗。需在module.json5中申请ohos.permission.SYSTEM_FLOAT_WINDOW权限。

在HarmonyOS Next中,可以通过Window组件和手势事件实现全局悬浮框。以下是核心步骤:

  1. 使用Window组件创建悬浮窗口,设置type: WindowType.TYPE_FLOAT并启用isLayoutFullScreen: false以允许拖动。
  2. 通过onTouch事件监听拖动操作,实时更新窗口位置(setWindowLayout)。
  3. 在拖动结束时(TouchType.UP),计算窗口中心与屏幕边缘的距离,自动贴靠最近边缘(例如通过display.getDefault().width获取屏幕宽度,动态调整x坐标)。
  4. 使用WindowManager管理窗口生命周期,确保全局可见。

关键代码示例(ArkTS):

// 创建悬浮窗口
let windowClass = window.create(context, "floatWindow", WindowType.TYPE_FLOAT);
windowClass.moveTo(initialX, initialY);

// 处理拖动
onTouch(event: TouchEvent) {
  if (event.type === TouchType.MOVE) {
    // 更新窗口位置
    windowClass.moveTo(event.offsetX, event.offsetY);
  } else if (event.type === TouchType.UP) {
    // 计算贴边:比较窗口中心与屏幕边缘距离,调整至最近边缘
    let screenWidth = display.getDefault().width;
    let centerX = event.offsetX + windowWidth / 2;
    let targetX = centerX < screenWidth / 2 ? 0 : screenWidth - windowWidth;
    windowClass.moveTo(targetX, event.offsetY);
  }
}

注意:需申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,并在module.json5中声明。

回到顶部