第 8 章 通知与窗口(上)

212 阅读44分钟

本章聚焦 HarmonyOS 开发中的通知与窗口技术。在通知方面,Notification Kit 提供本地通知发布能力,开发者不仅要掌握通知的发布、更新、取消等基础操作,还要学会管理通知通道、分组、角标以及添加行为意图,以满足多样化的通知需求。实况窗作为一种特殊通知形态,借助 Live View Kit 可展示实时信息,其多种模板适用于不同场景。

窗口管理部分,介绍了应用窗口的分类,包括主窗口和子窗口,以及窗口沉浸式能力和悬浮窗的概念及应用。开发者需要掌握在不同场景下设置主窗口和子窗口的方法,如加载页面、设置属性等,同时学会利用沉浸式能力提升用户体验,并通过监听窗口事件来处理业务逻辑。

最后,通过实现进度条通知和验证码登录两个案例,将理论知识与实践相结合,详细阐述了从代码结构、公共文件与资源,到具体功能实现的全过程,帮助开发者深入理解并运用这些技术进行 HarmonyOS 应用开发。

8.1 通知与实况窗

Notification Kit 为开发者提供本地通知发布通道,通知可用于显示消息、推送等,有多种表现形式和结构。开发者需请求通知授权,才能发布文本、进度等类型的通知,还能对通知进行更新、取消、分组管理,以及管理通知角标和添加行为意图。

Live View Kit 支持应用展示实时状态信息的实况窗,具有时段性、时效性和变化性,有胶囊态和卡片态两种展示形式,提供多种样式模板。开发者可通过 liveViewManager 构建、更新和结束本地实况窗,不过本地操作依赖应用进程,更推荐使用 Push Kit 进行更新或结束操作。

8.1.1 用户通知

Notification Kit(用户通知服务)为开发者提供本地通知发布通道,开发者可借助Notification Kit将应用产生的通知直接在客户端本地推送给用户,本地通知根据通知类型及发布场景会产生对应的铃声、震动、横幅、锁屏、息屏、通知栏提醒和显示。

8.1.1.1 通知介绍

通知旨在让用户以合适的方式及时获得有用的新消息,帮助用户高效地处理任务。应用可以通过通知接口发送通知消息,用户可以通过通知栏查看通知内容,也可以点击通知来打开应用,通知主要有以下使用场景:

  • 显示接收到的短消息、即时消息等。
  • 显示应用的推送消息,如广告、版本更新等。
  • 显示当前正在进行的事件,如下载等。
  1. 通知表现形式

通知会在不同场景以不同形式提示用户,例如通知在状态栏上显示为图标、在通知栏上会显示通知详细信息。重要的信息还可以使用横幅通知,浮动在界面顶部显示。

  1. 通知结构

下面以基础的文本通知为例,介绍通知的基本结构。

① 通知小图标:表示通知的功能与类型。

② 通知名称:应用名称或功能名称。

③ 时间:发送通知的时间,系统默认显示。

④ 展开箭头:点击标题区,展开被折叠的内容和按钮。若无折叠的内容和按钮,不显示此箭头。

⑤ 内容标题:描述简明概要。

⑥ 内容详情:描述具体内容或详情。

8.1.1.2 请求通知授权

应用需要获取用户授权才能发送通知。在通知发布前调用requestEnableNotification()方法,弹窗让用户选择是否允许发送通知,后续再次调用requestEnableNotification()方法时,则不再弹窗。开发步骤如下:

步骤一: 导入NotificationManager模块。

import { notificationManager } from '@kit.NotificationKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { common } from '@kit.AbilityKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 请求通知授权。

可通过requestEnableNotification的错误码判断用户是否授权。若返回的错误码为1600004,即为拒绝授权。

let context = getContext(this) as common.UIAbilityContext
notificationManager.isNotificationEnabled().then((data: boolean) => {
  hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data))
  if(!data){
    notificationManager.requestEnableNotification(context).then(() => {
      hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`)
    }).catch((err : BusinessError) => {
      if(1600004 == err.code){
        hilog.error(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`)
      } else {
        hilog.error(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`)
      }
    })
  }
}).catch((err : BusinessError) => {
    hilog.error(DOMAIN_NUMBER, TAG, `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`)
})

8.1.1.3 发布通知

本节将介绍几种常见类型通知的创建,在创建通知前需要先导入notificationManager模块,该模块提供通知管理的能力,包括发布、取消发布通知,创建、获取、移除通知通道等能力。

  1. 发布文本类型通知

文本类型通知主要应用于发送短信息、提示信息等,支持普通文本类型和多行文本类型。开发步骤如下:

步骤一: 导入模块。

import { notificationManager } from '@kit.NotificationKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 构造NotificationRequest对象,并发布通知。

  • 普通文本类型通知由标题、文本内容和附加信息三个字段组成,其中标题和文本内容是必填字段,大小均需要小于200字节,超出部分会被截断。
let notificationRequest: notificationManager.NotificationRequest = {
  id: 1,
  content: {
    notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知
    normal: {
      title: 'test_title',
      text: 'test_text',
      additionalText: 'test_additionalText',
    }
  }
}
notificationManager.publish(notificationRequest, (err: BusinessError) => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to publish notification. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.')
})
  • 多行文本类型通知继承了普通文本类型的字段,同时新增了多行文本内容、内容概要和通知展开时的标题,其字段均小于200字节,超出部分会被截断。通知默认显示与普通文本相同,展开后,标题显示为展开后标题内容,多行文本内容多行显示。
let notificationRequest: notificationManager.NotificationRequest = {
  id: 3,
  content: {
    notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_MULTILINE, // 多行文本类型通知
    multiLine: {
      title: 'test_title',
      text: 'test_text',
      briefText: 'test_briefText',
      longTitle: 'test_longTitle',
      lines: ['line_01', 'line_02', 'line_03', 'line_04'],
    }
  }
}
// 发布通知
notificationManager.publish(notificationRequest, (err: BusinessError) => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to publish notification. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.')
})
  1. 发布进度类型通知

进度条通知也是常见的通知类型,主要应用于文件下载、事务处理进度显示。当前系统提供了进度条模板,发布通知应用设置好进度条模板的属性值,如模板名、模板数据,通过通知子系统发送到通知栏显示。

目前系统模板仅支持进度条模板,通知模板NotificationTemplate中的data参数为用户自定义数据,用于显示与模块相关的数据。开发步骤如下:

步骤一: 导入模块。

import { notificationManager } from '@kit.NotificationKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 查询系统是否支持进度条模板,查询结果为支持downloadTemplate模板类通知。

notificationManager.isSupportTemplate('downloadTemplate').then((data:boolean) => {
  hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in supporting download template notification.')
  let isSupportTpl: boolean = data // isSupportTpl的值为true表示支持downloadTemplate模板类通知,false表示不支持
}).catch((err: BusinessError) => {
  hilog.error(DOMAIN_NUMBER, TAG, `Failed to support download template notification. Code is ${err.code}, message is ${err.message}`)
})

查询系统支持进度条模板后,再进行后续的步骤操作。

步骤三: 构造进度条模板对象,并发布通知。

let notificationRequest: notificationManager.NotificationRequest = {
  id: 5,
  content: {
    notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
    normal: {
      title: 'test_title',
      text: 'test_text',
      additionalText: 'test_additionalText'
    }
  },
  // 构造进度条模板,name字段当前需要固定配置为downloadTemplate
  template: {
    name: 'downloadTemplate',
    data: { title: 'File Title', fileName: 'music.mp4', progressValue: 45 }
  }
}

// 发布通知
notificationManager.publish(notificationRequest, (err: BusinessError) => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to publish notification. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.')
})

8.1.1.4 更新通知

在发出通知后,使用您之前使用的相同通知ID,再次调用notificationManager.publish来实现通知的更新。如果之前的通知是关闭的,将会创建新通知。

8.1.1.5 取消通知

用户收到通知提醒后,点击通知并拉起应用到前台时,应用可以选择取消某条通知或所有通知。

例如,用户收到某个好友的IM消息,点击通知进入应用查看消息后,应用可以取消相关通知提醒。

  • 通过通知ID和通知标签取消已发布的通知。

notificationManager.cancel(notificationId)

  • 取消所有已发布的通知。

notificationManager.cancelAll()

本文以取消文本类型通知为例进行说明,其他类型通知取消操作与此类似。

步骤一: 导入模块。

import { notificationManager } from '@kit.NotificationKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 发布通知。

参考 “8.1.1.3 发布通知”。

步骤三: 取消通知。

 // 当拉起应用到前台,查看消息后,调用该接口取消通知。
 notificationManager.cancel(1, (err: BusinessError) => {
   if (err) {
     hilog.error(DOMAIN_NUMBER, TAG, `Failed to cancel notification. Code is ${err.code}, message is ${err.message}`)
     return
   }
   hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in canceling notification.')
 })

8.1.1.6 管理通知通道

系统支持多种通知渠道,不同通知渠道对应的通知提醒方式不同,可以根据应用的实际场景选择适合的通知渠道,并对通知渠道进行管理(支持创建、查询、删除等操作)。

通知通道类型主要有以下几种:

  • SlotType.SOCIAL_COMMUNICATION:社交类型,状态栏中显示通知图标,有横幅和提示音。
  • SlotType.SERVICE_INFORMATION:服务类型,状态栏中显示通知图标,没有横幅但有提示音。
  • SlotType.CONTENT_INFORMATION:内容类型,状态栏中显示通知图标,但没有横幅或提示音。
  • SlotType.OTHER_TYPES:其它类型,状态栏中不显示通知图标,且没有横幅或提示音。

具体开发步骤如下:

步骤一: 导入notificationManager模块。

import { notificationManager } from '@kit.NotificationKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 创建指定类型的通知渠道。

// addslot回调
let addSlotCallBack = (err: BusinessError): void => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `addSlot failed, code is ${err.code}, message is ${err.message}`)
  } else {
    hilog.info(DOMAIN_NUMBER, TAG, `addSlot success`)
  }
}
notificationManager.addSlot(notificationManager.SlotType.SOCIAL_COMMUNICATION, addSlotCallBack)

步骤三: 查询指定类型的通知渠道。

获取对应渠道是否创建以及该渠道支持的通知提醒方式,比如是否有声音提示,是否有震动,锁屏是否可见等。

// getSlot回调
let getSlotCallback = (err: BusinessError, data: notificationManager.NotificationSlot): void => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to get slot. Code is ${err.code}, message is ${err.message}`)
  } else {
    hilog.info(DOMAIN_NUMBER, TAG, `Succeeded in getting slot.`)
    if (data != null) {
      hilog.info(DOMAIN_NUMBER, TAG, `slot enable status is ${JSON.stringify(data.enabled)}`)
      hilog.info(DOMAIN_NUMBER, TAG, `slot level is ${JSON.stringify(data.level)}`)
      hilog.info(DOMAIN_NUMBER, TAG, `vibrationEnabled status is ${JSON.stringify(data.vibrationEnabled)}`)
      hilog.info(DOMAIN_NUMBER, TAG, `lightEnabled status is ${JSON.stringify(data.lightEnabled)}`)
    }
  }
}
let slotType: notificationManager.SlotType = notificationManager.SlotType.SOCIAL_COMMUNICATION
notificationManager.getSlot(slotType, getSlotCallback)

步骤四: 删除指定类型的通知渠道。

// removeSlot回调
let removeSlotCallback = (err: BusinessError): void => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `removeSlot failed, code is ${JSON.stringify(err.code)}, message is ${JSON.stringify(err.message)}`)
  } else {
    hilog.info(DOMAIN_NUMBER, TAG, "removeSlot success")
  }
}
let slotType: notificationManager.SlotType = notificationManager.SlotType.SOCIAL_COMMUNICATION
notificationManager.removeSlot(slotType, removeSlotCallback)

8.1.1.7 创建通知组

将不同类型的通知分为不同的组,以便用户可以更好的管理他们。当同组的通知有多条的时候,会自动折叠起来,避免通知比较多的时候,通知界面比较杂乱,例如当通知栏里有聊天消息通知和商品推荐通知时,我们只需要通过设置字段groupName,就可以对通知进行分组,给groupName设置不同的值可以将通知分为不同的组。

可以使用groupName来指定通知组来实现,示例代码如下:

let notifyId = 0
 
let chatRequest: notificationManager.NotificationRequest = {  
  id: notifyId++, 
  groupName:'ChatGroup', 
  content: { 
    //... 
   } 
 }
 
let productRequest: notificationManager.NotificationRequest = {  
  id: notifyId++, 
  groupName: 'ProductGroup', 
  content: { 
    //... 
   } 
 }

8.1.1.8 管理通知角标

针对未读的通知,系统提供了角标设置接口,将未读通知个数显示在桌面图标的右上角角标上。

通知增加时,角标上显示的未读通知个数需要增加。

通知被查看后,角标上显示的未读通知个数需要减少,没有未读通知时,不显示角标。

具体开发步骤如下:

步骤一: 导入NotificationManager模块。

import { notificationManager } from '@kit.NotificationKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 增加角标个数。

发布通知在NotificationRequest的badgeNumber字段里携带,可参考通知发布章节。

示例为调用setBadgeNumber接口增加角标,在发布完新的通知后,调用该接口。

let setBadgeNumberCallback = (err: BusinessError): void => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to set badge number. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, `Succeeded in setting badge number.`)
}

let badgeNumber = 9
notificationManager.setBadgeNumber(badgeNumber, setBadgeNumberCallback)

步骤三: 减少角标个数。

一条通知被查看后,应用需要调用接口设置剩下未读通知个数,桌面刷新角标。

let setBadgeNumberCallback = (err: BusinessError): void => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to set badge number. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, `Succeeded in setting badge number.`)
}

let badgeNumber = 8
notificationManager.setBadgeNumber(badgeNumber, setBadgeNumberCallback)

8.1.1.9 为通知添加行为意图

WantAgent提供了封装行为意图的能力,这里所说的行为意图主要是指拉起指定的应用组件及发布公共事件等能力。给通知添加行为意图后,点击通知后可以拉起指定的UIAbility或者发布公共事件,可以按照以下步骤来实现:

步骤一: 导入模块。

import { notificationManager } from '@kit.NotificationKit'
import { wantAgent, WantAgent } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

const TAG: string = '[PublishOperation]'
const DOMAIN_NUMBER: number = 0xFF00

步骤二: 创建WantAgentInfo信息。

场景一:创建拉起UIAbility的WantAgent的WantAgentInfo信息。

let wantAgentObj:WantAgent // 用于保存创建成功的wantAgent对象,后续使用其完成触发的动作。

// 通过WantAgentInfo的operationType设置动作类型
let wantAgentInfo:wantAgent.WantAgentInfo = {
  wants: [
    {
      deviceId: '',
      bundleName: 'com.samples.notification',
      abilityName: 'SecondAbility',
      action: '',
      entities: [],
      uri: '',
      parameters: {}
    }
  ],
  actionType: wantAgent.OperationType.START_ABILITY,
  requestCode: 0,
  wantAgentFlags:[wantAgent.WantAgentFlags.CONSTANT_FLAG]
}

场景二:创建发布公共事件的WantAgent的WantAgentInfo信息。

let wantAgentObj: WantAgent // 用于保存创建成功的WantAgent对象,后续使用其完成触发的动作。

// 通过WantAgentInfo的operationType设置动作类型
let wantAgentInfo:wantAgent.WantAgentInfo = {
  wants: [
    {
      action: 'event_name', // 设置事件名
      parameters: {},
    }
  ],
  actionType: wantAgent.OperationType.SEND_COMMON_EVENT,
  requestCode: 0,
  wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG],
}

步骤三: 调用getWantAgent()方法进行创建WantAgent。

// 创建WantAgent
wantAgent.getWantAgent(wantAgentInfo, (err: BusinessError, data:WantAgent) => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to get want agent. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in getting want agent.')
  wantAgentObj = data
})

步骤四: 构造NotificationRequest对象,并发布WantAgent通知。

// 构造NotificationRequest对象
let notificationRequest: notificationManager.NotificationRequest = {
  content: {
    notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
    normal: {
      title: 'Test_Title',
      text: 'Test_Text',
      additionalText: 'Test_AdditionalText',
    },
  },
  id: 6,
  label: 'TEST',
  // wantAgentObj使用前需要保证已被赋值(即步骤3执行完成)
  wantAgent: wantAgentObj,
}

notificationManager.publish(notificationRequest, (err: BusinessError) => {
  if (err) {
    hilog.error(DOMAIN_NUMBER, TAG, `Failed to publish notification. Code is ${err.code}, message is ${err.message}`)
    return
  }
  hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.')
})

步骤五: 发布WantAgent通知。

// 发布通知
notificationManager.publish(notificationRequest).then(() => {
  console.info("publish success")
}).catch((err: Error) => { 
  console.error(`publish failed, ${err.code}, ${err.message}`) 
})

用户通过点击通知栏上的通知,即可触发WantAgent的动作。

8.1.2 实况窗

Live View Kit(实况窗服务)支持应用将订单或者服务的实时状态信息变化在设备的关键界面展示,并对展示信息的生命周期、用户界面UI效果等进行管理。

实况窗是一种帮助用户聚焦正在进行的任务,方便快速查看和即时处理的通知形态,具有时段性、时效性、变化性的特点。

在展示形态上,实况窗支持在锁屏、通知中心、状态栏等位置展示,主要有两种展示形式:胶囊态和卡片态。

知中心状态栏锁屏

8.1.2.1 构建本地实况窗简介

开发者可以通过liveViewManager模块构建本地实况窗,完成实况窗的整个生命周期流程(包括创建、更新与结束)。请注意,只有应用在前台运行,即用户实际使用应用并且产生了服务合约的情况下,开发者才可以创建实况窗;与此同时,本地更新或结束实况窗依赖于开发者的应用进程,所以我们更推荐开发者在本地创建实况窗后使用Push Kit更新或结束实况窗。

8.1.2.2 导入liveViewManager

在项目中导入liveViewManager,并新建实况窗控制类(例如LiveViewController),构造isLiveViewEnabled()方法,用于校验实况窗开关(设置>应用和元服务>应用名>实况窗)是否打开。打开实况窗开关是创建实况窗的前提条件。示例代码如下:

import { liveViewManager } from '@kit.LiveViewKit'

export class LiveViewController {
  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }
}

8.1.2.3 创建实况窗

实况窗根据扩展区不同共有5种样式模板:进度可视化模板、强调文本模板、左右文本模板、赛事比分模板和导航模板。

  1. 进度可视化模板

进度可视化模板适用于打车、外卖等场景。

示例代码如下:

构建LiveViewController后,在代码中初始化LiveViewController并调LiveViewController.startLiveView()方法。

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView()
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): 
    Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "DELIVERY", // 实况窗的应用场景。DELIVERY:即时配送(外卖、生鲜)
      liveViewData: {
        primary: {
          title: "骑手已接单",
          content: [
            { text: "距商家 " },
            { text: "300 ", textColor: "#FF0A59F7" },
            { text: "米 | " },
            { text: "3 ", textColor: "#FF0A59F7" },
            { text: "分钟到店" }
          ], // 所有文本仅能设置为一种颜色,不设置textColor时,默认展示#FF000000
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 40,
            color: "#FF317AF7",
            backgroundColor: "#f7819ae0",
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_UP,
            // 进度条指示器图标,
            // 取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            indicatorIcon: "indicator.png", 
            lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
            // 进度条每个节点图标,
            //取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            nodeIcons: ["icon_1.png", "icon_2.png", "icon_3.png"]
          }
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}
  1. 强调文本模板

强调文本模板适用于取餐、排队等场景。

示例代码如下:

构建LiveViewController后,请在代码中初始化LiveViewController并调用LiveViewController.startLiveView()方法。

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView()
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "PICK_UP", // 实况窗的应用场景。PICK_UP:取餐。
      liveViewData: {
        primary: {
          title: "餐品已备好",
          content: [
            { text: "请前往" },
            { text: " XXX店 ", textColor: "#FF0A59F7" },
            { text: "取餐" },
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
            title: "取餐码",
            content: "72988",
            underlineColor: "#FF0A59F7",
            descPic: "coffee.png" // 扩展区右侧产品描述图,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
          }
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    }
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}
  1. 左右文本模板

左右文本模板适用于高铁、航班等场景。

示例代码如下:

构建LiveViewController后,请在代码中初始化LiveViewController并调用LiveViewController.startLiveView()方法。

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "TRAIN", // 实况窗的应用场景。TRAIN:高铁/火车。
      liveViewData: {
        primary: {
          title: "列车检票提醒",
          content: [
            { text: "检票口 " },
            { text: "6B ", textColor: "#FF0A59F7" },
            { text: "| 座位 " },
            { text: "03车 12F", textColor: "#FF0A59F7" }
          ], // 所有文本仅能设置为一种颜色,不设置textColor时,默认展示#FF000000
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(), // 点击实况窗默认动作。
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_FLIGHT,
            firstTitle: "09:00",
            firstContent: "上海虹桥",
            lastTitle: "14:20",
            lastContent: "汉口",
            spaceIcon: "icon.png", // 扩展区中间间隔图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            isHorizontalLineDisplayed: true,
            additionalText: "以上信息仅供参考" // 扩展区底部内容,仅可用于左右文本模板。
          }
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}
  1. 赛事比分模板

赛事比分模板适用于赛事场景。

示例代码如下:

构建LiveViewController后,请在代码中初始化LiveViewController并调用LiveViewController.startLiveView()方法。

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView()
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "SCORE", // 实况窗的应用场景。SCORE:赛事比分。
      liveViewData: {
        primary: {
          title: "第四节比赛中",
          content: [
            { text: "XX", textColor:"#FF0A59F7" },
            { text: " VS " },
            { text: "XX", textColor:"#FF0A59F7" },
            { text: " | " },
            { text: "小组赛 第五场", textColor:"#FF0A59F7" }
          ],
          keepTime: 1,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_SCORE,
            hostName: "队名A",
            hostIcon: "host.png", // 扩展区左侧图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            hostScore: "110",
            guestName: "队名B",
            guestIcon: "guest.png", // 扩展区右侧图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            guestScore: "102",
            competitionDesc: [
              { text: "●", textColor: "#FFFF0000" },
              { text: "Q4" }
            ],
            competitionTime: "02:16",
            isHorizontalLineDisplayed: true
          }
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}
  1. 导航模板

导航模板适用于出行导航场景。

示例代码如下:

构建LiveViewController后,请在代码中初始化LiveViewController并调用LiveViewController.startLiveView()方法。

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView()
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "NAVIGATION", // 实况窗的应用场景。NAVIGATION:导航。
      liveViewData: {
        primary: {
          title: "178米后左转",
          content: [
            { text: "去往"},
            { text: " 南京东路", textColor: "#FF0A59F7" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_NAVIGATION,
            currentNavigationIcon: "navigation.png", // 当前导航方向,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            navigationIcons: ["left.png","straight.png","straight.png","right.png"] // 导航方向的箭头集合图片,每个元素取值为“/resources/rawfile”路径下的文件名或image.PixelMap
          }
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    }
    const agent = await wantAgent.getWantAgent(wantAgentInfo);
    return agent
  }
}
  1. 实况胶囊

说明

胶囊形态各模板参数固定,与创建实况窗时的模板类型无关。可创建的胶囊类型有:文本胶囊、计时器胶囊、进度胶囊。

除了实况窗卡片形态,开发者还需考虑实况窗胶囊形态的展示效果。若开发者创建实况窗时还想同步创建实况窗胶囊,则需在liveViewManager.LiveView(结构体)中携带胶囊所需的参数liveViewData.capsule(不同胶囊类型携带不同的参数)。示例代码如下:

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "TAXI", // 实况窗的应用场景。TAXI:出行打车。
      liveViewData: {
        primary: {
          title: "司机预计5分钟后到达",
          content: [
            { text: "白", textColor: "#FF0A59F7" },
            { text: "●" },
            { text: "沪AXXXXXX", textColor: "#FF0A59F7" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 30,
            color: "#ff0959F8",
            backgroundColor: "#ffc9d7e4",
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_UP,
            indicatorIcon: "indicator.png", // 进度条指示器图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            lineType: liveViewManager.LineType.LINE_TYPE_NORMAL_SOLID_LINE,
            nodeIcons: ["icon_1.png", "icon_2.png", "icon_3.png"] // 进度条节点图标集合,每个元素取值为“/resources/rawfile”路径下的文件名或image.PixelMap
          }
        },
        // 实况胶囊相关参数
        capsule: {
          type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
          status: 1,
          icon: "capsule_store.png", // 胶囊图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
          backgroundColor: "#ff0959F8",
          title: "5分钟"
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}
  1. 实况窗计时器

实况窗计时器适用于排队、抢票等场景。

开发者若需要使用实况窗计时器,则需在liveViewManager.LiveView(结构体)中的配置timer字段,并在当前支持的字段中使用占位符: ${placeholder.timer}

例如:固定区的文本内容中使用占位符,系统将替代占位符为实况窗计时器。

示例代码如下:

构建LiveViewController后,请在代码中初始化LiveViewController并调用LiveViewController.startLiveView()方法。

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit'

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView)
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "QUEUE", // 实况窗的应用场景。QUEUE:排队
      timer: {
        time: 620000,
        isCountdown: false,
        isPaused: false
      },
      liveViewData: {
        primary: {
          title: "大桌4人等位  32桌",
          content: [
            { text: "已等待 " }, 
            { text: "${placeholder.timer}", textColor:"#ff10c1f7" },
            { text: " | 预计还需>30分钟" }
          ], // 所有文本仅能设置为一种颜色,不设置textColor时,默认展示#FF000000
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 0,
            color: "#FFFF0000",
            backgroundColor: "#FF000000",
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_OVERLAY,
            indicatorIcon: "indicator.png", // 进度条指示器图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
            nodeIcons: ["icon_1.png","icon_2.png"] // 进度条节点图标集合,每个元素取值为“/resources/rawfile”路径下的文件名或image.PixelMap
          }
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}
  1. 点击实况窗动作

请调用wantAgent.getWantAgent()构造点击动作字段所需的参数值,当前实况窗支持的点击动作如下:

  • 点击实况窗的默认动作:在liveViewManager.LiveView(结构体)中携带胶囊所需的参数liveViewData.primary.clickAction字段。
  • 点击辅助区的跳转动作:在liveViewManager.LiveView(结构体)中携带胶囊所需的参数liveViewData.primary.extensionData.clickAction字段。

8.1.2.4 本地更新和结束实况窗

调用liveViewManager.isLiveViewEnabled()确认实况窗开关打开后,调用liveViewManager的updateLiveView更新实况窗,调用stopLiveView结束实况窗。更新时需要修改请求体中对应的参数。示例代码如下:

import { liveViewManager } from '@kit.LiveViewKit'
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  private static contentColor: string = '#FF000000'
  private static capsuleColor: string = '#FF308977'

  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView()
    return await liveViewManager.startLiveView(defaultView)
  }

  public async updateLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 修改实况窗内容
    const defaultView = await LiveViewController.buildDefaultView()
    defaultView.liveViewData.primary.title = "预计23:49送达"
    defaultView.liveViewData.primary.content = [
      { text: "等待商家接单, " },
      { text: "03:20", textColor: "#FFFF9C4F" },
      { text: " 未接单自动取消" },
    ]
    defaultView.liveViewData.primary.layoutData = {
      layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
      progress: 0,
      lineType: 0,
      // 进度条节点图标集合,
      // 每个元素取值为“/resources/rawfile”路径下的文件名或image.PixelMap
      nodeIcons: [
        'icon_store_white.png',
        'icon_finish.png'
      ]
    }
    defaultView.liveViewData.capsule = {
      type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
      status: 1,
      // 实况胶囊的图标,
      // 取值为“/resources/rawfile”路径下的文件名或image.PixelMap
      icon: 'capsule_store.png', 
      backgroundColor: LiveViewController.capsuleColor,
      title: "待接单"
    }
    // 更新实况窗
    return await liveViewManager.updateLiveView(defaultView)
  }

  public async stopLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!await LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.")
    }
    // 修改实况窗内容
    const defaultView = await LiveViewController.buildDefaultView();
    defaultView.liveViewData.primary.title = '商品已送达'
    defaultView.liveViewData.primary.content = [
      { text: '感谢您的认可,' },
      { text: '期待下一次光临' }
    ];
    defaultView.liveViewData.primary.layoutData = {
      layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
      progress: 100,
      lineType: 0,
      // 进度条节点图标集合,
      // 每个元素取值为“/resources/rawfile”路径下的文件名或image.PixelMap
      nodeIcons: [
        'icon_order.png',
        'icon_finish.png'
      ] 
    }
    defaultView.liveViewData.capsule = {
      type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
      status: 1,
      icon: 'capsule_gps.png', 
      // 实况胶囊的图标,取值为“/resources/rawfile”路径下的文件名或image.PixelMap
      backgroundColor: LiveViewController.capsuleColor,
      title: '已送达'
    }
    // 结束实况窗
    return await liveViewManager.stopLiveView(defaultView)
  }

  private static async buildDefaultView(): 
  Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "DELIVERY", // 实况窗的应用场景。DELIVERY:即时配送(外卖、生鲜)
      liveViewData: {
        primary: {
          title: "餐品待支付",
          content: [
            { text: "咖啡 ", textColor: "#FF0A59F7" },
            { text: "等2件商品" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
            title: "待支付金额",
            content: "25.5元",
            underlineColor: "#FF0A59F7",
            // 扩展区右侧产品描述图,
            // 取值为“/resources/rawfile”路径下的文件名或image.PixelMap
            descPic: "coffee.png" 
          }
        },
        // 实况胶囊相关参数
        capsule: {
          type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
          status: 1,
          icon: "capsule_store.png",
          // 实况胶囊的图标,
          // 取值为“/resources/rawfile”路径下的文件名或image.PixelMap
          backgroundColor: "#FF308977",
          title: "待支付"
        }
      }
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled()
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    }
    const agent = await wantAgent.getWantAgent(wantAgentInfo)
    return agent
  }
}

8.2 管理应用窗口

窗口模块用于在同一块物理屏幕上,提供多个应用界面显示、交互的机制。

  • 对应用开发者而言,窗口模块提供了界面显示和交互能力。
  • 对终端用户而言,窗口模块提供了控制应用界面的方式。
  • 对整个操作系统而言,窗口模块提供了不同应用界面的组织管理逻辑。

HarmonyOS的窗口模块将窗口界面分为系统窗口、应用窗口两种基本类型。

  • 系统窗口:系统窗口指完成系统特定功能的窗口。如音量条、壁纸、通知栏、状态栏、导航栏等。
  • 应用窗口:应用窗口区别于系统窗口,指与应用显示相关的窗口。根据显示内容的不同,应用窗口又分为应用主窗口、应用子窗口两种类型。
    • 应用主窗口:应用主窗口用于显示应用界面,会在"任务管理界面"显示。
    • 应用子窗口:应用子窗口用于显示应用的弹窗、悬浮窗等辅助窗口,不会在"任务管理界面"显示。应用子窗口的生命周期跟随应用主窗口。

应用窗口模式指应用主窗口启动时的显示方式。HarmonyOS目前支持全屏、分屏、自由窗口三种应用窗口模式。这种对多种应用窗口模式的支持能力,也称为操作系统的“多窗口能力”。

  • 全屏:应用主窗口启动时铺满整个屏幕。
  • 分屏:应用主窗口启动时占据屏幕的某个部分,当前支持二分屏。两个分屏窗口之间具有分界线,可通过拖拽分界线调整两个部分的窗口尺寸。
  • 自由窗口:自由窗口的大小和位置可自由改变。同一个屏幕上可同时显示多个自由窗口,这些自由窗口按照打开或者获取焦点的顺序在Z轴排布。当自由窗口被点击或触摸时,将导致其Z轴高度提升,并获取焦点。

8.2.1 基本概念

Stage模型下应用窗口管理的两个概念:

  • 窗口沉浸式能力:指对状态栏、导航栏等系统窗口进行控制,减少状态栏导航栏等系统界面的突兀感,从而使用户获得最佳体验的能力。

沉浸式能力只在应用主窗口作为全屏窗口时生效。通常情况下,应用子窗口(弹窗、悬浮窗口等辅助窗口)和处于自由窗口下的应用主窗口无法使用沉浸式能力。

  • 悬浮窗:全局悬浮窗口是一种特殊的应用窗口,具备在应用主窗口和对应Ability退至后台后仍然可以在前台显示的能力。

悬浮窗口可以用于应用退至后台后,使用小窗继续播放视频,或者为特定的应用创建悬浮球等快速入口。应用在创建悬浮窗口前,需要申请对应的权限。

8.2.2 场景介绍

在Stage模型下,管理应用窗口的典型场景有:

  • 设置应用主窗口属性及目标页面
  • 设置应用子窗口属性及目标页面
  • 体验窗口沉浸式能力
  • 设置悬浮窗
  • 监听窗口不可交互与可交互事件

8.2.3 设置应用主窗口

在Stage模型下,应用主窗口由UIAbility创建并维护生命周期。在UIAbility的onWindowStageCreate回调中,通过WindowStage获取应用主窗口,即可对其进行属性设置等操作。还可以在应用配置文件中设置应用主窗口的属性,如最大窗口宽度maxWindowWidth等。

开发步骤如下:

步骤一: 获取应用主窗口。

通过getMainWindow接口获取应用主窗口。

步骤二: 设置主窗口属性。

可设置主窗口的背景色、亮度值、是否可触等多个属性,开发者可根据需要选择对应的接口。本示例以设置“是否可触”属性为例。

步骤三: 为主窗口加载对应的目标页面。

通过loadContent接口加载主窗口的目标页面。

import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 1.获取应用主窗口。
    let windowClass: window.Window | null = null
    windowStage.getMainWindow((err: BusinessError, data) => {
      let errCode: number = err.code;
      if (errCode) {
        console.error(JSON.stringify(err))
        return
      }
      windowClass = data
      console.info(JSON.stringify(data))
      // 2.设置主窗口属性。以设置"是否可触"属性为例。
      let isTouchable: boolean = true
      windowClass.setWindowTouchable(isTouchable, (err: BusinessError) => {
        let errCode: number = err.code
        if (errCode) {
          console.error(JSON.stringify(err))
          return;
        }
        console.info('Succeeded in setting the window to be touchable.')
      })
    })
    // 3.为主窗口加载对应的目标页面。
    windowStage.loadContent("pages/page2", (err: BusinessError) => {
      let errCode: number = err.code
      if (errCode) {
        console.error(JSON.stringify(err))
        return;
      }
      console.info('Succeeded in loading the content.')
    });
  }
};

8.2.4 设置应用子窗口

开发者可以按需创建应用子窗口,如弹窗等,并对其进行属性设置等操作。

开发步骤如下:

步骤一: 创建应用子窗口。

通过createSubWindow接口创建应用子窗口。

步骤二: 设置子窗口属性。

子窗口创建成功后,可以改变其大小、位置等,还可以根据应用需要设置窗口背景色、亮度等属性。

步骤三: 加载显示子窗口的具体内容。

通过setUIContent和showWindow接口加载显示子窗口的具体内容。

步骤四: 销毁子窗口。

当不再需要某些子窗口时,可根据具体实现逻辑,使用destroyWindow接口销毁子窗口。

直接在onWindowStageCreate里面创建子窗口的整体示例代码如下:

import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

let windowStage_: window.WindowStage | null = null
let sub_windowClass: window.Window | null = null

export default class EntryAbility extends UIAbility {
  showSubWindow() {
    // 1.创建应用子窗口。
    if (windowStage_ == null) {
      console.error('Failed to create the subwindow. Cause: windowStage_ is null')
    }
    else {
      windowStage_.createSubWindow("mySubWindow", (err: BusinessError, data) => {
        let errCode: number = err.code
        if (errCode) {
          console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err))
          return
        }
        sub_windowClass = data
        console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data))
        // 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
        sub_windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to move the window. Cause:' + JSON.stringify(err))
            return
          }
          console.info('Succeeded in moving the window.')
        })
        sub_windowClass.resize(500, 500, (err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to change the window size. Cause:' + JSON.stringify(err))
            return
          }
          console.info('Succeeded in changing the window size.')
        });
        // 3.为子窗口加载对应的目标页面。
        sub_windowClass.setUIContent("pages/page3", (err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to load the content. Cause:' + JSON.stringify(err))
            return;
          }
          console.info('Succeeded in loading the content.')
          // 3.显示子窗口。
          (sub_windowClass as window.Window).showWindow((err: BusinessError) => {
            let errCode: number = err.code
            if (errCode) {
              console.error('Failed to show the window. Cause: ' + JSON.stringify(err))
              return
            }
            console.info('Succeeded in showing the window.')
          })
        })
      })
    }
  }

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

  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage_ = windowStage
    // 开发者可以在适当的时机,如主窗口上按钮点击事件等,创建子窗口。并不一定需要在onWindowStageCreate调用,这里仅作展示
    this.showSubWindow()
  }

  onWindowStageDestroy() {
    // 开发者可以在适当的时机,如子窗口上点击关闭按钮等,销毁子窗口。并不一定需要在onWindowStageDestroy调用,这里仅作展示
    this.destroySubWindow()
  }
};

另外,也可以在某个page页面通过点击按钮创建子窗口,整体示例代码如下:

// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage) {
  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      console.error('Failed to load the content. Cause:' + JSON.stringify(err))
      return
    }
    console.info('Succeeded in loading the content.')
  })

  // 给Index页面传递windowStage
  AppStorage.setOrCreate('windowStage', windowStage)
}
// Index.ets
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

let windowStage_: window.WindowStage | undefined = undefined
let sub_windowClass: window.Window | undefined = undefined

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  private CreateSubWindow(){
    // 获取windowStage
    windowStage_ = AppStorage.get('windowStage')
    // 1.创建应用子窗口。
    if (windowStage_ == null) {
      console.error('Failed to create the subwindow. Cause: windowStage_ is null')
    }
    else {
      windowStage_.createSubWindow("mySubWindow", (err: BusinessError, data) => {
        let errCode: number = err.code
        if (errCode) {
          console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err))
          return
        }
        sub_windowClass = data
        console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data))
        // 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
        sub_windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to move the window. Cause:' + JSON.stringify(err))
            return
          }
          console.info('Succeeded in moving the window.')
        })
        sub_windowClass.resize(500, 500, (err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to change the window size. Cause:' + JSON.stringify(err))
            return
          }
          console.info('Succeeded in changing the window size.')
        });
        // 3.为子窗口加载对应的目标页面。
        sub_windowClass.setUIContent("pages/subWindow", (err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to load the content. Cause:' + JSON.stringify(err))
            return
          }
          console.info('Succeeded in loading the content.')
          // 3.显示子窗口。
          (sub_windowClass as window.Window).showWindow((err: BusinessError) => {
            let errCode: number = err.code
            if (errCode) {
              console.error('Failed to show the window. Cause: ' + JSON.stringify(err))
              return
            }
            console.info('Succeeded in showing the window.')
          })
        })
      })
    }
  }
  private destroySubWindow(){
    // 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
    (sub_windowClass as window.Window).destroyWindow((err: BusinessError) => {
      let errCode: number = err.code
      if (errCode) {
        console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err))
        return
      }
      console.info('Succeeded in destroying the window.')
    })
  }
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button(){
          Text('CreateSubWindow')
          .fontSize(24)
          .fontWeight(FontWeight.Normal)
        }.width(220).height(68)
        .margin({left:10, top:60})
        .onClick(() => {
          this.CreateSubWindow()
        })
        Button(){
          Text('destroySubWindow')
          .fontSize(24)
          .fontWeight(FontWeight.Normal)
        }.width(220).height(68)
        .margin({left:10, top:60})
        .onClick(() => {
          this.destroySubWindow()
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}
// subWindow.ets
@Entry
@Component
struct SubWindow {
  @State message: string = 'Hello subWindow'
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

8.2.5 体验窗口沉浸式能力

在看视频、玩游戏等场景下,用户往往希望隐藏状态栏、导航栏等不必要的系统窗口,从而获得更佳的沉浸式体验。此时可以借助窗口沉浸式能力(窗口沉浸式能力都是针对应用主窗口而言的),达到预期效果。从API version 10开始,沉浸式窗口默认配置为全屏大小并由组件模块控制布局,状态栏、导航栏背景颜色为透明,文字颜色为黑色;应用窗口调用setWindowLayoutFullScreen接口,设置为true表示由组件模块控制忽略状态栏、导航栏的沉浸式全屏布局,设置为false表示由组件模块控制避让状态栏、导航栏的非沉浸式全屏布局。

开发步骤如下:

步骤一: 获取应用主窗口。

通过getMainWindow接口获取应用主窗口。

步骤二: 实现沉浸式效果。有以下两种方式:

  • 方式一:应用主窗口为全屏窗口时,调用setWindowSystemBarEnable接口,设置导航栏、状态栏不显示,从而达到沉浸式效果。
  • 方式二:调用setWindowLayoutFullScreen接口,设置应用主窗口为全屏布局;然后调用setWindowSystemBarProperties接口,设置导航栏、状态栏的透明度、背景/文字颜色以及高亮图标等属性,使之保持与主窗口显示协调一致,从而达到沉浸式效果。

步骤三: 加载显示沉浸式窗口的具体内容。

通过loadContent接口加载沉浸式窗口的具体内容。

import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 1.获取应用主窗口。
    let windowClass: window.Window | null = null
    windowStage.getMainWindow((err: BusinessError, data) => {
      let errCode: number = err.code
      if (errCode) {
        console.error('Failed to obtain the main window. Cause: ' + JSON.stringify(err))
        return
      }
      windowClass = data
      console.info('Succeeded in obtaining the main window. Data: ' + JSON.stringify(data))

      // 2.实现沉浸式效果。方式一:设置导航栏、状态栏不显示。
      let names: Array<'status' | 'navigation'> = []
      windowClass.setWindowSystemBarEnable(names)
        .then(() => {
          console.info('Succeeded in setting the system bar to be visible.')
        })
        .catch((err: BusinessError) => {
          console.error('Failed to set the system bar to be visible. Cause:' + JSON.stringify(err))
        });
      // 2.实现沉浸式效果。方式二:设置窗口为全屏布局,配合设置导航栏、状态栏的透明度、背景/文字颜色及高亮图标等属性,与主窗口显示保持协调一致。
      let isLayoutFullScreen = true
      windowClass.setWindowLayoutFullScreen(isLayoutFullScreen)
        .then(() => {
          console.info('Succeeded in setting the window layout to full-screen mode.')
        })
        .catch((err: BusinessError) => {
          console.error('Failed to set the window layout to full-screen mode. Cause:' + JSON.stringify(err))
        })
      let sysBarProps: window.SystemBarProperties = {
        statusBarColor: '#ff00ff',
        navigationBarColor: '#00ff00',
        // 以下两个属性从API Version 8开始支持
        statusBarContentColor: '#ffffff',
        navigationBarContentColor: '#ffffff'
      }
      windowClass.setWindowSystemBarProperties(sysBarProps)
        .then(() => {
          console.info('Succeeded in setting the system bar properties.')
        })
        .catch((err: BusinessError) => {
          console.error('Failed to set the system bar properties. Cause: ' + JSON.stringify(err))
        })
    })
    // 3.为沉浸式窗口加载对应的目标页面。
    windowStage.loadContent("pages/page2", (err: BusinessError) => {
      let errCode: number = err.code
      if (errCode) {
        console.error('Failed to load the content. Cause:' + JSON.stringify(err))
        return
      }
      console.info('Succeeded in loading the content.')
    })
  }
}

8.2.6 设置悬浮窗(受限开放)

悬浮窗可以在已有的任务基础上,创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台,悬浮窗仍然可以在前台显示。通常悬浮窗位于所有应用窗口之上,开发者可以创建悬浮窗,并对悬浮窗进行属性设置等操作。

开发步骤如下:

前提条件: 创建WindowType.TYPE_FLOAT即悬浮窗类型的窗口,需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,该权限为受控开放权限,仅符合指定场景的在2in1设备上的应用可申请该权限。

步骤一: 创建悬浮窗。

通过window.createWindow接口创建悬浮窗类型的窗口。

步骤二: 对悬浮窗进行属性设置等操作。

悬浮窗窗口创建成功后,可以改变其大小、位置等,还可以根据应用需要设置悬浮窗背景色、亮度等属性。

步骤三: 加载显示悬浮窗的具体内容。

通过setUIContent和showWindow接口加载显示悬浮窗的具体内容。

步骤四: 销毁悬浮窗。

当不再需要悬浮窗时,可根据具体实现逻辑,使用destroyWindow接口销毁悬浮窗。

import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 1.创建悬浮窗。
    let windowClass: window.Window | null = null;
    let config: window.Configuration = {
      name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: this.context
    }
    window.createWindow(config, (err: BusinessError, data) => {
      let errCode: number = err.code
      if (errCode) {
        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(300, 300, (err: BusinessError) => {
        let errCode: number = err.code
        if (errCode) {
          console.error('Failed to move the window. Cause:' + JSON.stringify(err))
          return
        }
        console.info('Succeeded in moving the window.')
      });
      windowClass.resize(500, 500, (err: BusinessError) => {
        let errCode: number = err.code
        if (errCode) {
          console.error('Failed to change the window size. Cause:' + JSON.stringify(err))
          return
        }
        console.info('Succeeded in changing the window size.')
      })
      // 3.为悬浮窗加载对应的目标页面。
      windowClass.setUIContent("pages/page4", (err: BusinessError) => {
        let errCode: number = err.code
        if (errCode) {
          console.error('Failed to load the content. Cause:' + JSON.stringify(err))
          return
        }
        console.info('Succeeded in loading the content.')
        // 3.显示悬浮窗。
        (windowClass as window.Window).showWindow((err: BusinessError) => {
          let errCode: number = err.code
          if (errCode) {
            console.error('Failed to show the window. Cause: ' + JSON.stringify(err))
            return
          }
          console.info('Succeeded in showing the window.')
        })
      })
      // 4.销毁悬浮窗。当不再需要悬浮窗时,可根据具体实现逻辑,使用destroy对其进行销毁。
      windowClass.destroyWindow((err: BusinessError) => {
        let errCode: number = err.code
        if (errCode) {
          console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err))
          return
        }
        console.info('Succeeded in destroying the window.')
      })
    })
  }
}

8.2.7 监听窗口不可交互与可交互事件

应用在前台显示过程中可能会进入某些不可交互的场景,比较典型的是进入多任务界面。此时,对于一些应用可能需要选择暂停某个与用户正在交互的业务,如视频类应用暂停正在播放的视频或者相机暂停预览流等。而当该应用从多任务又切回前台时,又变成了可交互的状态,此时需要恢复被暂停中断的业务,如恢复视频播放或相机预览流等。

开发步骤如下:

在创建WindowStage对象后可通过监听'windowStageEvent'事件类型,监听到窗口进入前台、后台、前台可交互、前台不可交互等事件,应用可根据这些上报的事件状态进行相应的业务处理。

import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    try {
      windowStage.on('windowStageEvent', (data) => {
        console.info('Succeeded in enabling the listener for window stage event changes. Data: ' +
                     JSON.stringify(data))

        // 根据事件状态类型选择进行相应的处理
        if (data == window.WindowStageEventType.SHOWN) {
          console.info('current window stage event is SHOWN')
          // 应用进入前台,默认为可交互状态
          // ...
        } else if (data == window.WindowStageEventType.HIDDEN) {
          console.info('current window stage event is HIDDEN')
          // 应用进入后台,默认为不可交互状态
          // ...
        } else if (data == window.WindowStageEventType.PAUSED) {
          console.info('current window stage event is PAUSED')
          // 前台应用进入多任务,转为不可交互状态
          // ...
        } else if (data == window.WindowStageEventType.RESUMED) {
          console.info('current window stage event is RESUMED')
          // 进入多任务后又继续返回前台时,恢复可交互状态
          // ...
        }
        // ...
      })
    } catch (exception) {
      console.error('Failed to enable the listener for window stage event changes. Cause:' +
                    JSON.stringify(exception))
    }
  }
}