【鸿蒙实战开发】基于子窗口实现应用内悬浮窗

546 篇文章 5 订阅
435 篇文章 3 订阅

场景描述

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就能创建并使用子窗口。

鸿蒙全栈开发全新学习指南

之前总有很多小伙伴向我反馈说,不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以这里为大家准备了一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

①全方位,更合理的学习路径
路线图包括ArkTS基础语法、鸿蒙应用APP开发、鸿蒙能力集APP开发、次开发多端部署开发、物联网开发等九大模块,六大实战项目贯穿始终,由浅入深,层层递进,深入理解鸿蒙开发原理!

②多层次,更多的鸿蒙原生应用
路线图将包含完全基于鸿蒙内核开发的应用,比如一次开发多端部署、自由流转、元服务、端云一体化等,多方位的学习内容让学生能够高效掌握鸿蒙开发,少走弯路,真正理解并应用鸿蒙的核心技术和理念。

③实战化,更贴合企业需求的技术点
学习路线图中的每一个技术点都能够紧贴企业需求,经过多次真实实践,每一个知识点、每一个项目,都是码牛课堂鸿蒙研发团队精心打磨和深度解析的成果,注重对学生的细致教学,每一步都确保学生能够真正理解和掌握。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:https://qr21.cn/FV7h05

如何快速入门:

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05

大厂鸿蒙面试题::https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值