首页 > 其他分享 >鸿蒙(HarmonyOS)实战开发篇——基于子窗口实现应用内悬浮窗

鸿蒙(HarmonyOS)实战开发篇——基于子窗口实现应用内悬浮窗

时间:2024-09-27 21:23:54浏览次数:11  
标签:窗口 鸿蒙 err 悬浮 pipController 画中画 HarmonyOS event

推荐看点

场景描述

app应用会使用悬浮窗/悬浮球的方式来给用户展示一些应用重要&便捷功能的入口,类似android和iOS应用中常见的应用内可拖拽的悬浮球和小窗口视频悬浮窗,点击悬浮窗修改悬浮窗样式和响应事件跳转页面,在跳转页面后依然可以显示在屏幕中上个页面拖拽后的固定位置等。

应用经常会遇到如下的业务诉求:

场景一:通过事件添加和移除悬浮窗,悬浮窗样式可定制(暂定两种,无白边圆球形和小视频播放窗口类型),可代码修改位置和布局。
场景二:创建悬浮窗后,主窗口的系统侧滑返回事件可正常使用。
场景三:可响应正常点击事件,可通过触发拖动使悬浮窗的移动,根据最后手势停留位置,做动画靠屏幕左或靠右显示,跳转和返回上级页面后悬浮窗依然存在,且相对手机屏幕位置不变。
场景四:悬浮窗内组件事件触发主窗口的页面跳转(Router和Navigation两种都要有)。
场景五:悬浮窗的窗口大小自适应组件,子窗口中页面设置了宽高,需要让子窗口自适应页面组件大小。
场景六:支持控制悬浮窗隐藏和销毁。
场景七:视频类应用主动调用画中画完成后台播放,以及返回桌面时自动启动画中画。

方案描述

场景一:

通过事件添加和移除悬浮窗,悬浮窗样式可定制(暂定两种,无白边圆球形和小视频播放窗口类型),可代码修改位置和布局。

效果图

方案

通过子窗口创建windowStage.createSubWindow(‘mySubWindow’),和windowClass.setWindowLayoutFullScreen去除白边。

核心代码

在EntryAbility中获取WindowStage。

onWindowStageCreate(windowStage: window.WindowStage): void {

  // Main window is created, set main page for this ability

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

  windowStage.loadContent('pages/Page', (err, data) => {

  if (err.code) {

  hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');

  return;

}

// 保存窗口管理器

AppStorage.setOrCreate("windowStage", windowStage);

hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');

});

}

创建子窗口,子窗口样式由子窗口加载的页面组件样式决定。

this.windowStage.createSubWindow("mySubWindow", (err, windowClass) => {

  if (err.code > 0) {

    console.error("failed to create subWindow Cause:" + err.message)

    return;

  }

  try {

    // 设置子窗口加载页

    windowClass.setUIContent("pages/MySubWindow", () => {

      windowClass.setWindowBackgroundColor("#00000000")

    });

    // 设置子窗口左上角坐标

    windowClass.moveWindowTo(0, 200)

    // 设置子窗口大小

    windowClass.resize(vp2px(75), vp2px(75))

    // 展示子窗口

    windowClass.showWindow();

    // 设置子窗口全屏化布局不避让安全区

    windowClass.setWindowLayoutFullScreen(true);

  } catch (err) {

    console.error("failed to create subWindow Cause:" + err)

  }

})

场景二:

创建悬浮窗后,主窗口的系统侧滑返回事件可正常使用。

效果图

方案

通过window.shiftAppWindowFocus转移窗口焦点实现创建子窗口后,主窗口依然可以响应事件。核心代码

在子窗口中将焦点转移到主窗口。

onPageShow(): void {

  setTimeout(() => {

  // 获取子窗口ID

  let subWindowID: number = window.findWindow("mySubWindow").getWindowProperties().id

  // 获取主窗口ID

  let mainWindowID: number = this.windowStage.getMainWindowSync().getWindowProperties().id

  // 将焦点从子窗口转移到主窗口

  window.shiftAppWindowFocus(subWindowID, mainWindowID)

}, 500)

}

场景三:

可响应正常点击事件,可通过拖动触发悬浮窗的拖拽移动,根据最后手势停留位置,做动画靠屏幕左或靠右显示,跳转和返回上级页面后悬浮窗依然存在,且相对手机屏幕位置不变。

效果图

方案

通过设置手势顺序模式识别PanGesture,实现拖拽悬浮窗。

核心代码

创建Position。

interface Position {

  x: number,

  y: number

}

设置拖拽选项。

private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });

通过在子窗口父组件绑定拖拽动作完成悬浮窗坐标移动。

.gesture(

  // 声明该组合手势的类型为Sequence类型

  PanGesture(this.panOption)

    .onActionStart((event: GestureEvent) => {

      console.info('Pan start');

    })// 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition

    .onActionUpdate((event: GestureEvent) => {

      this.windowPosition.x += event.offsetX;

      this.windowPosition.y += event.offsetY;

      this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y)

    })

    .onActionEnd((event: GestureEvent) => {

      // 贴边判断

      if (event.offsetX > 0) {

        this.windowPosition.x = display.getDefaultDisplaySync().width - this.subWindow.getWindowProperties()

          .windowRect

          .width;

      } else if (event.offsetX < 0) {

        this.windowPosition.x = 0;

      }

      this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y)

      console.info('Pan end');

    })

)

场景四:

悬浮窗内组件事件触发主窗口的页面跳转(Router和Navigation两种都要有)。

方案

通过获取窗口上下文,实现在悬浮窗点击后,实现主窗口Router跳转。

通过配置NavPathStack全局变量,实现主窗口navigation跳转 。

核心代码

通过windowStage获取主窗口的Router,实现主窗口的Router跳转。

.onClick((event: ClickEvent) => {

  this.windowStage.getMainWindowSync()

    .getUIContext()

    .getRouter()

    .back()

})

通过AppStorage获取NavPathStack,实现主窗口navigation跳转。

.onClick((event: ClickEvent) => {

  let navPath = AppStorage.get("pageInfos") as NavPathStack;

  navPath.pushPath({ name: 'pageOne' })

})

场景五:

悬浮窗的窗口大小自适应组件,子窗口中页面设置了宽高,需要让子窗口自适应页面组件大小。

效果图

方案

通过监听通用事件ComponentObserver,设置window的resize调整窗口大小。

核心代码

查找子窗口。

@State subWindow: window.Window = window.findWindow("mySubWindow");

注册监听事件。

//监听id为COMPONENT_ID的组件回调事件listener: inspector.ComponentObserver = inspector.createComponentObserver('COMPONENT_ID');

通过onClick()事件,实现对组件变化的监听。

if (this.flag) {

  Image($r("app.media.voice2"))

    .id("COMPONENT_ID")

    .borderRadius(5)

    .width(75)

    .height(75)

    .onClick(() => {

      // 设置图标切换标识

      this.flag = !this.flag

      this.listener.on('layout', () => {

        // 监听布局变更后调整子窗大小

        this.subWindow.resize(componentUtils.getRectangleById("COMPONENT_ID").size.width,

          componentUtils.getRectangleById("COMPONENT_ID").size.height)

      })

    })

} else {

  Image($r("app.media.voice"))

    .id("COMPONENT_ID")

    .borderRadius(50)

    .width(100)

    .height(100)

    .onClick(() => {

      this.flag = !this.flag

      this.listener.on('layout', () => {

        this.subWindow.resize(componentUtils.getRectangleById("COMPONENT_ID").size.width,

          componentUtils.getRectangleById("COMPONENT_ID").size.height)

      })

    })

场景六:

支持控制悬浮窗隐藏和销毁。

效果图

方案

通过设置窗口windowClass.minimize和windowClass.destroyWindow,实现悬浮窗的隐藏和销毁。

核心代码

通过调用minimize,实现子窗口最小化。

.onClick((event: ClickEvent) => {

  this.subWindow.minimize()

})

通过实现destroyWindow,实现子窗口的资源销毁。

// 通过查找子窗口名称对子窗口进行销毁

window.findWindow("mySubWindow").destroyWindow()

场景七:

视频类应用主动调用画中画完成后台播放,以及返回桌面时自动启动画中画。

效果图

方案

1.通过pipController.startPiP()完成主动调用画中画功能。

2.通过pipController.setAutoStartEnabled(true)在返回桌面时完成全局画中画播放。

核心代码

创建XComponent组件。

XComponent({ id: 'pipDemo', type: 'surface', controller: this.mXComponentController })

  .onLoad(() => {

    this.surfaceId = this.mXComponentController.getXComponentSurfaceId();

    // 需要设置AVPlayer的surfaceId为XComponentController的surfaceId

    this.player = new AVPlayerDemo(this.surfaceId);

    this.player.avPlayerFdSrcDemo();

  })

  .onDestroy(() => {

    console.info(`[${TAG}] XComponent onDestroy`);

  })

  .size({ width: '100%', height: '800px' })

创建pipWindowController和startPip方法。

startPip() {

  if (!pipWindow.isPiPEnabled()) {

    console.error(`picture in picture disabled for current OS`);

    return;

  }

  let config: pipWindow.PiPConfiguration = {

    context: getContext(this),

    componentController: this.mXComponentController,

    // 当前page导航id

    navigationId: this.navId,

    // 对于视频通话、视频会议等场景,需要设置相应的模板类型

    templateType: pipWindow.PiPTemplateType.VIDEO_PLAY,

    // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例

    contentWidth: 800,

    // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例

    contentHeight: 600,

  };

  // 步骤1:创建画中画控制器,通过create接口创建画中画控制器实例

  let promise: Promise<pipWindow.PiPController> = pipWindow.create(config);

  promise.then((controller: pipWindow.PiPController) => {

    this.pipController = controller;

    // 步骤1:初始化画中画控制器

    this.initPipController();

    // 步骤2:通过startPiP接口启动画中画

    this.pipController.startPiP().then(() => {

      console.info(`Succeeded in starting pip.`);

    }).catch((err: BusinessError) => {

      console.error(`Failed to start pip. Cause:${err.code}, message:${err.message}`);

    });

  }).catch((err: BusinessError) => {

    console.error(`Failed to create pip controller. Cause:${err.code}, message:${err.message}`);

  });

}

初始化pipWindowController。

initPipController() {

  if (!this.pipController) {

    return;

  }

  // 通过setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画,注册stateChange和controlPanelActionEvent回调

  this.pipController.setAutoStartEnabled(true/*or true if necessary*/); // 默认为false

  this.pipController.on('stateChange', (state: pipWindow.PiPState, reason: string) => {

    this.onStateChange(state, reason);

  });

  this.pipController.on('controlPanelActionEvent', (event: pipWindow.PiPActionEventType) => {

    this.onActionEvent(event);

  });

}

完成画中画播放使用stopPip方法停止。

stopPip() {

  if (this.pipController) {

    let promise: Promise<void> = this.pipController.stopPiP();

    promise.then(() => {

      console.info(`Succeeded in stopping pip.`);

      this.pipController?.off('stateChange'); // 如果已注册stateChange回调,停止画中画时取消注册该回调

      this.pipController?.off('controlPanelActionEvent'); // 如果已注册controlPanelActionEvent回调,停止画中画时取消注册该回调

    }).catch((err: BusinessError) => {

      console.error(`Failed to stop pip. Cause:${err.code}, message:${err.message}`);

    });

  }

}

其他常见问题

Q:windowStage怎么获取?

A:WindowStage需要在EntryAbility中的onWindowStageCreate中用AppStorage.setOrCreate()获取。

Q:子窗口可以用于应用外么?

A:子窗口只能在应用内使用。

Q:子窗口的默认大小是多大?

A:子窗口默认不设置大小的话是除安全区外的屏幕区域。

Q:UIExtension可以用子窗口么?

A:UIExtension不是窗口对象,没有办法调用窗口接口。

Q:Har和Hsp中可以使用子窗口么?

A:只要能获取到windowStage就能创建并使用子窗口。

标签:窗口,鸿蒙,err,悬浮,pipController,画中画,HarmonyOS,event
From: https://blog.csdn.net/lc748258/article/details/142528010

相关文章

  • 鸿蒙(HarmonyOS)实战开发篇——基于ArkUI现有能力实现自定义弹窗封装方案
    推荐看点鸿蒙(HarmonyOS)应用层开发(北向)知识点汇总【OpenHarmony】鸿蒙南向开发保姆级知识点汇总~持续更新中……场景描述自定义弹窗是应用开发需要实现的基础功能,包括但不限于HarmonyOS开发者文档中定义的模态、半模态、Toast等形式,封装一个好用且和UI组件解耦的弹窗组......
  • HarmonyOS NEXT-CoreVision Kit-FaceDetector-实现人脸识别,获取人脸数据
    效果演示图,右边的是人脸数据,可用来比对人脸注意这里只有真机才能测试是否成功,测试机型pce-w30实现这个效果很简洁:打开相册、选取图片、打开文件、创建imageSource、创建PixelMap、喂给faceDetector拿到结果在这里我简单封装了两个工具类方便后续使用,分别是:照片选择类、......
  • 华为申请鸿蒙甄选、鸿蒙优选商标,加词的注意!
    近日华为在35类广告销售上申请鸿蒙智选、鸿蒙优选、鸿蒙精品,鸿蒙甄选等商标,后面所加的词智选、优选、精品、甄选等基本上是属于通用词。 这样在35类拿到鸿蒙+通用词商标,需要先拿到“鸿蒙“商标,经普推知产商标老杨检索发现,早在2019年华为就申请鸿蒙35类商标,拿下部分核心小类......
  • 华为云技术专家分享4大举措,助力开发者开启鸿蒙原生应用开发
    摘要:鸿蒙生态是面向全场景时代的新生态,为开发者带来新价值新机遇。本文分享自华为云开发者联盟公众号《DTSETechTalk|第66期:鸿蒙上云,加速开发者成长。》本期DTSETechTalk直播主题是《鸿蒙上云,加速开发者成长》,华为云HarmonyOSDTSE技术布道师芝诺在本议题中与开发者们交流华......
  • HarmonyOS开发之WaterFlow组件
    在HarmonyOS应用开发中,瀑布流布局因其灵活性和美观性而广受欢迎。HarmonyOSNEXT提供了强大的WaterFlow组件,可以帮助开发者轻松实现瀑布流布局,并支持多种自定义布局和性能优化特性。本文将通过两个具体场景,详细介绍如何使用WaterFlow组件实现页面滑动加载和吸顶效果。场景一:瀑......
  • 鸿蒙(HarmonyOS)--声明式UI、自定义组件
    目录1.基础语法概述2.声明式UI描述2.1创建组件2.1.1无参数2.1.2有参数2.2配置属性2.3配置事件 2.4配置子组件3.自定义组件3.1创建自定义组件3.1.1基本使用3.1.2组件属性、方法3.1.3通用样式事件 3.2页面和自定义组件生命周期3.2.1自定义组件的创建......
  • 京东金融APP的鸿蒙之旅:技术、挑战与实践
    一、背景在今年6月份的华为开发者大会上,华为宣布HarmonyOSNEXT面向开发者和先锋用户启动Beta升级,并将于今年四季度正式商用。在9月21日的华为2024全联接大会上华为终端总裁宣布,已有超过1万个应用和元服务上架HarmonyOSNEXT应用市场。此外,华为每年投入超过60亿元人民币激励开......
  • 京东金融APP的鸿蒙之旅:技术、挑战与实践
    一、背景在今年6月份的华为开发者大会上,华为宣布HarmonyOSNEXT面向开发者和先锋用户启动Beta升级,并将于今年四季度正式商用。在9月21日的华为2024全联接大会上华为终端总裁宣布,已有超过1万个应用和元服务上架HarmonyOSNEXT应用市场。此外,华为每年投入超过60亿元人民币激励开发者......
  • 鸿蒙应用开发——单独去除某个地图上的某个marker标记
    鸿蒙应用开发——单独去除某个地图上的某个标记一、将每一个创建的marker标记添加到数组中保存 @Statemarkers?:Array<map.Marker>=[]; ...... leticon=$r('app.media.busStopSign'); letmarkerOptions:mapCommon.MarkerOptions={ position:{ //标点经纬度......
  • 鸿蒙应用开发——Scroll/List组件无法触发滑动,检查子组件的高度是否被固定/是否内嵌了
    鸿蒙应用开发——Scroll/List组件无法触发滑动一、检查子组件的高度是否被固定若Scroll/List组件的子组件的高度超出了Scroll/List组件高度则能够滚动,此时子组件的高度固定且不超过Scroll/List组件的高度的话,就无法滚动。这种情况直接取消子组件的固定高度即可,例如去掉height:'1......