【HarmonyOS NEXT】全局弹窗的一种实现思路

1,326 阅读4分钟

一、写在前

参考android的使用,我们在有一些场景会用到自定义弹窗的使用,比如:

  1. 展示布局在主Ability界面上,webview,信息输入框等
  2. 期望ui不影响原Ability生命周期
  3. 封装成一个弹窗工具类,放置在har包当中

但是目前外放文档提供自定义弹窗路由的使用都暂时没法满足我们的需求

二、解决方法

通过查阅文档和测试,发现有一个api可以比较完美的解决我们以上的问题,参考这里

简单的思路步骤是这样的:

  1. 通过ability的onWindowStageCreate的生命周期接口,将windowStage传入har包的类当中,然后通过windowStage.createSubWindow的api创建子window窗口,
  2. sub_windowClass.moveWindowTo和sub_windowClass.resize接口可以控制窗口的左上角的点和窗口大小,又非全屏+旋转需求可以使用
  3. sub_windowClass.setUIContent这里需要设置一个ets页面,因此这里可以实现你想要的界面内容
  4. sub_windowClass.showWindow和sub_windowClass.destroyWindow可以控制窗口的展示和关闭,这里可以作为工具类暴露接口

三、其他需要处理的问题

主要有以下三点要注意:

  1. 工具类和ets页面的通信
  2. 旋转后布局的处理
  3. page页面的配置

通信的话采用Emitter进行线程间通信

旋转后的布局,可以根据实际需要计算调整,如果非全屏还需要结合sub_windowClass.moveWindowTo和sub_windowClass.resize接口来调整windows的位置

page页面的配置,指的是虽然ets页面可以放置在har包当中,但是page还是需要到src/main/resources/base/profile/main_pages.json配置一下,才能正常使用,否则就是空白页面

四、实现效果

QQ20240610-222124-HD.gif

五、源码实现

DialogUtil.ts


import window from '@ohos.window';
import emitter from '@ohos.events.emitter';

let TAG:string = "DialogUtil"

export class DialogUtil {
    private static inst: DialogUtil = new DialogUtil();
    private windowStage!: window.WindowStage;
    private sub_windowClass!: window.Window;
    private windowClass!: window.Window;

    constructor() {
    }

    static getInst(): DialogUtil{
        return this.inst;
    }

    onWindowStageCreate(windowStage: window.WindowStage){
        this.windowStage = windowStage;
        windowStage?.getMainWindow((err, data) => {
            if (err.code) {
                return;
            }
            this.windowClass = data;
        });
    }

    onWindowStageDestroy() {
        // Main window is destroyed, release UI related resources
        this.hide();
    }

    show(x?:number, y?:number, width?:number, height?: number){
        this.init();
        // 1.创建应用子窗口。
        this.windowStage?.createSubWindow("mySubWindow", (err, data) => {
            if (err.code) {
                console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err));
                return;
            }
            this.sub_windowClass = data;
            console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
            // 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
            this.sub_windowClass?.moveWindowTo(x?x:0, y?y:0, (err) => {
                if (err.code) {
                    console.error('Failed to move the window. Cause:' + JSON.stringify(err));
                    return;
                }
                console.info('Succeeded in moving the window.');
            });
            this.sub_windowClass?.resize(width?width:this.windowClass.getWindowProperties().windowRect.width, height?height:this.windowClass.getWindowProperties().windowRect.height, (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.为子窗口加载对应的目标页面。
            this.sub_windowClass?.setUIContent("pages/dialog", (err) => {
                if (err.code) {
                    console.error('Failed to load the content. Cause:' + JSON.stringify(err));
                    return;
                }
                console.info('Succeeded in loading the content.');
                // 3.显示子窗口。
                this.sub_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.');
                });
            });
        })
    }

    hide(){
        // 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
        this.sub_windowClass?.destroyWindow((err) => {
            if (err.code) {
                console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
                return;
            }
            console.info('Succeeded in destroying the window.');
        });
    }

    init(){
        //监听旋转修改子窗口大小
        emitter.on({eventId: 1}, (eventData) => {
            if (eventData["data"]) {
                this.sub_windowClass?.moveWindowTo(eventData["data"].width/10, eventData["data"].height/10, (err) => {
                    if (err.code) {
                        console.error('Failed to move the window. Cause:' + JSON.stringify(err));
                        return;
                    }
                    console.info('Succeeded in moving the window.');
                });
                this.sub_windowClass?.resize(eventData["data"].width/10*8, eventData["data"].height/10*8, (err) => {
                    if (err.code) {
                        console.debug(TAG, 'Failed to change the window size. Cause:' + JSON.stringify(err));
                        return;
                    }
                    console.debug(TAG, 'Succeeded in changing the window size.');
                });
            }
        });
        //监听关闭窗口
        emitter.on({eventId: 0}, (eventData) => {
            if (eventData["data"]) {
                this.hide()
            }
        });
    }
}

dialog.ets

import emitter from '@ohos.events.emitter'
import display from '@ohos.display'
@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)

        Button() {
          Text("关闭弹窗")
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
        }.onClick(()=>{
          emitter.emit({
            eventId: 0,
            priority: emitter.EventPriority.LOW
          }, {});
        })
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(Color.Green)
  }

  aboutToAppear(){
    //屏幕监听,同时处理旋转后的视图
    let callback = (data:number) => {
      let displayClass:display.Display | null = null;
      //
      try {
        displayClass = display.getDefaultDisplaySync();
        console.info('Listening enabled. displayClass: ' + JSON.stringify(displayClass));
        let rotation:number = displayClass.rotation

        if (rotation == 0 || rotation == 2) {
          //根据旋转处理宽高等...
        }
        // 发送屏幕信息
        emitter.emit({
          eventId: 1,
          priority: emitter.EventPriority.LOW
        }, {
          data: {width:displayClass.width,height:displayClass.height}
        });

      } catch (exception) {
        console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
      }
    };
    try {
      display.on("change", callback);
    } catch (exception) {
      console.error('Failed to register callback. Code: ' + JSON.stringify(exception));
    }
  }
}

Index.ets

import { DialogUtil } from 'library/src/main/ets/utils/DialogUtil'
import display from '@ohos.display'
@Entry
@Component
struct Index {
  @State message: string = '打开弹窗'

  build() {
    Row() {
      Button() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }.onClick(()=>{
        let displayClass:display.Display = display.getDefaultDisplaySync();
        DialogUtil.getInst().show(displayClass.width/10,displayClass.height/10,displayClass.width/10*8,displayClass.height/10*8)}
      )
      .width('100%')
    }
    .height('100%')
  }
}

EntryAbility.ts

import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
import {DialogUtil} from "library/src/main/ets/utils/DialogUtil"

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
    });

    DialogUtil.getInst().onWindowStageCreate(windowStage);
  }

  onWindowStageDestroy() {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
    DialogUtil.getInst().onWindowStageDestroy()
  }

  onForeground() {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground() {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

六、结尾

实现仅供参考,如果有问题和建议,欢迎指出~

本文正在参加华为鸿蒙有奖征文征文活动