HarmonyOS鸿蒙Next中实践过的悬浮窗口

HarmonyOS鸿蒙Next中实践过的悬浮窗口

背景

悬浮视图或者窗体,在Android和iOS两大移动平台均有使用,HarmonyOS 也实现了此功能,如下为大家分享一下效果

准备

  1. 熟读HarmonyOS 悬浮窗口指导 https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/application-window-stage-0000001427584712-V3#ZH-CN_TOPIC_0000001523968638__%E8%AE%BE%E7%BD%AE%E6%82%AC%E6%B5%AE%E7%AA%97

  2. 熟读HarmonyOS 手势指导 https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-gesture-events-single-gesture-0000001450596854-V3#section128381857165115

  3. 熟读ALC签名指导,用于可以申请 “ohos.permission.SYSTEM_FLOAT_WINDOW” 权限 https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/signing-0000001587684945-V3#section157591551175916

  4. 拜读帖子 https://developer.huawei.com/consumer/cn/forum/topic/0202125832632308225?fid=0101587866109860105

实践总结

  1. 如果开启了悬浮窗口,任何界面的物理返回键事件都会被悬浮窗口拦截掉,即 手势返回废了

  2. 参数类型易混淆, 拖动 PanGesture 中的 onActionUpdate 接口,数据单位为 vp,window 中的 moveWindowTo 接口参数,数据单位为 px

  3. 采用 moveWindowTo 实现的窗口 拖动效果十分不平滑

  4. 通过 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) {
    let context = this.context;
    let atManager = abilityAccessCtrl.createAtManager();

    checkPermissions().then((result)=>{
      if(result){
        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
  }
}

悬浮窗口页面

/**
 *
 * 官方指导: https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/application-window-stage-0000001427584712-V3#ZH-CN_TOPIC_0000001523968638__设置悬浮窗
 * 参见帖子:https://developer.huawei.com/consumer/cn/forum/topic/0202125832632308225?fid=0101587866109860105
 *
 */

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
    });
  }

}

更多关于HarmonyOS鸿蒙Next中实践过的悬浮窗口的实战教程也可以访问 https://www.itying.com/category-93-b0.html

5 回复

是全局的吗?退到后台悬浮窗还在吗

更多关于HarmonyOS鸿蒙Next中实践过的悬浮窗口的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


标题

这是主要内容。


这是另一段内容。

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

全局的,退到后台依然还在,

在HarmonyOS鸿蒙Next中,悬浮窗口通过WindowManagerAbilitySlice实现。首先,创建WindowManager实例,并设置窗口参数如WindowManager.LayoutConfig。然后,通过WindowManageraddWindow方法添加悬浮窗口,并指定其布局文件。为确保窗口可交互,需处理触摸事件和生命周期管理。最后,通过removeWindow方法移除窗口。这一机制适用于需要临时显示或快速交互的场景,如通知、快捷操作等。

回到顶部