HarmonyOS鸿蒙Next中怎么实现全局window悬浮框
HarmonyOS鸿蒙Next中怎么实现全局window悬浮框 怎么实现全局的window悬浮框,可随意拖动,当拖动到中间时,会自动计算贴边显示
【背景知识】
设置应用子窗口:开发者可以按需创建应用子窗口,如弹窗等,并对其进行属性设置等操作。
【解决方案】
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设备上的应用可申请该权限。申请方式请参考:申请使用受限权限。
三、自己以组件的方式实现窗口悬浮 拖拽,缩放
- 申请悬浮窗权限
在module.json5
中配置权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
"reason": "需要悬浮窗权限",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
2. 创建悬浮窗并实现拖拽贴边
使用@kit.ArkUI
的window
模块创建悬浮窗,并在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
实现流程
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
组件和手势事件实现全局悬浮框。以下是核心步骤:
- 使用
Window
组件创建悬浮窗口,设置type: WindowType.TYPE_FLOAT
并启用isLayoutFullScreen: false
以允许拖动。 - 通过
onTouch
事件监听拖动操作,实时更新窗口位置(setWindowLayout
)。 - 在拖动结束时(
TouchType.UP
),计算窗口中心与屏幕边缘的距离,自动贴靠最近边缘(例如通过display.getDefault().width
获取屏幕宽度,动态调整x
坐标)。 - 使用
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
中声明。