背景
悬浮视图或者窗体,在Android和iOS两大移动平台均有使用,HarmonyOS 也实现了此功能,如下为大家分享一下效果
准备
-
熟读HarmonyOS 悬浮窗口指导
-
熟读HarmonyOS 手势指导
-
熟读ALC签名指导,用于可以申请 “ohos.permission.SYSTEM_FLOAT_WINDOW” 权限。
-
熟悉的文档在下方
鸿蒙OS开发 | 更多内容↓点击 | HarmonyOS与OpenHarmony技术 |
---|---|---|
鸿蒙技术文档 | 开发知识更新库gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md在这。 | 或+mau123789学习,是v喔 |
实践代码
-
如果开启了悬浮窗口,任何界面的物理返回键事件都会被悬浮窗口拦截掉,即 手势返回废了
-
参数类型易混淆, 拖动 PanGesture 中的onActionUpdate接口,数据单位为vp,window中的 moveWindowTo接口参数,数据单位为px
-
采用moveWindowTo实现的窗口拖动效果十分不平滑
-
通过 requestPermissionsFromUser 申请 ohos.permission.SYSTEM_FLOAT_WINDOW 权限时,无法弹出系统权限提示框
片段代码
配置module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
......
{
"name": "FloatWindowAbility",
"srcEntry": "./ets/myentryability/FloatWindowAbility.ts",
"description": "$string:FloatWindowAbility_desc",
"icon": "$media:icon",
"label": "$string:FloatWindowAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
},
],
"requestPermissions": [
{
"name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
"usedScene": {
"abilities": [
"FloatWindowAbility"
],
"when": "always"
}
}
]
}
}
悬浮窗口UIAbility
import window from '@ohos.window';
import BaseUIAbility from '../baseuiability/BaseUIAbility';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
const permissions: Array<Permissions> = ['ohos.permission.SYSTEM_FLOAT_WINDOW'];
export default class FloatWindowAbility extends BaseUIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
let context = this.context;
let atManager = abilityAccessCtrl.createAtManager();
checkPermissions().then((result)=>{
if(result){
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
let length: number = grantStatus.length;
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
console.log('用户授权,可以继续访问目标操作')
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
console.log('用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限')
return;
}
}
// 授权成功
// 1.创建悬浮窗。
let windowClass = null;
let config = {name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: this.context};
window.createWindow(config, (err, data) => {
if (err.code) {
console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data));
windowClass = data;
// 2.悬浮窗窗口创建成功后,设置悬浮窗的位置、大小及相关属性等。
windowClass.moveWindowTo(0, 200, (err) => {
if (err.code) {
console.error('Failed to move the window. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in moving the window.');
});
windowClass.resize(1080, 151, (err) => {
if (err.code) {
console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in changing the window size.');
});
// 3.为悬浮窗加载对应的目标页面。
windowClass.setUIContent("custompages/FloatPage", (err) => {
if (err.code) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading the content.');
// 3.显示悬浮窗。
windowClass.showWindow((err) => {
if (err.code) {
console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in showing the window.');
});
try {
windowClass.setWindowBackgroundColor('#00000000')
} catch (exception) {
console.error('Failed to set the background color. Cause: ' + JSON.stringify(exception));
}
});
})
}).catch((err) => {
console.error(`requestPermissionsFromUser failed, code is ${err.code}, message is ${err.message}`);
})
}
})
}
}
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
let atManager = abilityAccessCtrl.createAtManager();
let grantStatus: abilityAccessCtrl.GrantStatus;
// 获取应用程序的accessTokenID
let tokenId: number;
try {
let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
tokenId = appInfo.accessTokenId;
} catch (err) {
console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
}
// 校验应用是否被授予权限
try {
grantStatus = await atManager.checkAccessToken(tokenId, permission);
} catch (err) {
console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
}
return grantStatus;
}
async function checkPermissions(): Promise<boolean> {
const permissions: Array<Permissions> = ['ohos.permission.SYSTEM_FLOAT_WINDOW'];
let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions[0]);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
// 已经授权,可以继续访问目标操作
console.log('没有授权')
return true
} else {
// 申请日历权限
console.log('已授权')
return false
}
}
悬浮窗口页面
import common from '@ohos.app.ability.common';
import window from '@ohos.window';
@Entry
@Component
struct Index {
@State lasttime: number = 0
@State message: string = '悬浮窗'
@State foldStatus: boolean = false
@State idleName: string = '收起'
@State floatWindowWidth: number = 0
@State offsetX: number = 0
@State offsetY: number = 0
@State positionX: number = 0
@State positionY: number = 0
@State windowPosition: Position = { x: 0, y: 0 };
private context = getContext(this) as common.UIAbilityContext;
private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
floatWindow: window.Window
aboutToAppear(){
this.eventHubFunc()
this.floatWindow = window.findWindow("floatWindow")
this.floatWindowWidth = 1080
this.panOption.setDistance(1)
}
onBackPress(){
console.log('返回')
}
build() {
Row() {
Text('X').width(px2vp(140))
.textAlign(TextAlign.Center)
.fontColor(Color.Red).onClick(()=>{
//关闭所依赖的UIAbility
this.context.terminateSelf()
//销毁悬浮窗。当不再需要悬浮窗时,可根据具体实现逻辑,使用destroy对其进行销毁。
this.floatWindow.destroyWindow((err) => {
if (err.code) {
console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in destroying the window.');
});
})
Text(this.idleName)
.width(px2vp(140))
.height('100%')
.fontSize(18)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Gray)
.onClick(()=>{
this.foldStatus = !this.foldStatus
if(this.foldStatus){
this.idleName = "展开"
this.floatWindowWidth = 280
} else {
this.idleName = "收起"
this.floatWindowWidth = 1080
}
})
Divider().vertical(true).color(Color.Red)
if(!this.foldStatus) {
Text(this.message)
.width(px2vp(800))
.fontSize(18)
.fontColor(Color.White)
.padding('12vp')
}
}
.width(px2vp(this.floatWindowWidth))
.height(px2vp(150))
.borderRadius('12vp')
.backgroundColor(Color.Green)
.gesture(
// 绑定PanGesture事件,监听拖拽动作
PanGesture(this.panOption)
.onActionStart((event: GestureEvent) => {
console.info('Pan start');
})
// 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition
.onActionUpdate((event: GestureEvent) => {
console.log(event.offsetX +' ' + event.offsetY)
this.offsetX = this.positionX + event.offsetX
this.offsetY = this.positionY + event.offsetY
this.floatWindow.moveWindowTo(vp2px(this.offsetX), vp2px(this.offsetY));
})
.onActionEnd(() => {
this.positionX = this.offsetX
this.positionY = this.offsetY
console.info('Pan end');
})
)
}
eventHubFunc() {
this.context.eventHub.on('info', (data) => {
this.message = data
});
}
}