实况窗功能概述

实况窗是 HarmonyOS 提供的一种帮助用户聚焦正在进行的任务、方便快速查看和即时处理的通知形态。它支持应用将订单或服务的实时状态信息变化在设备的关键界面展示,并对展示信息的生命周期、用户界面 UI 效果等进行管理。实况窗具有时段性时效性变化性的特点:

  • 时段性:事件或服务需要持续一段时间,有明确的开始和结束,而非单点的提醒或信息。例如打车、外卖等从事件开始到结束需要经历一段时间,属于实况窗;天气提示、待办等单点提醒则不属于实况窗。
  • 时效性:内容为正在进行或即时发生的事件或服务的提醒,在特定时间段内,信息对用户有价值。例如打车行程中、外卖配送中等正在进行的用户活动;2 天后的机票在刚买时不提醒,而在出发前提示,属于实况窗。权限调用、功能待机等系统状态不属于实况窗。
  • 变化性:实况窗所展示的内容需要动态更新,以确保用户看到最新的状态。

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

产品优势

  • 面向 HarmonyOS 5 及以上的全量 Phone、Tablet 设备:实况窗特性与设备硬件完全解耦,开发者接入后,可以覆盖到所有 HarmonyOS 5 及以上的全量 Phone、Tablet 设备。
  • 一步接入多触达点展示:开发者一次接入,可以实现包括锁屏、通知中心、状态栏在内多触达点展示实况窗。
  • 不打断现有的操作,用户可及时关注服务进展:状态栏的实况胶囊支持点击交互,用户可以在任何界面查看实况胶囊或者点击实况胶囊展开卡片,查看详细进展。实况窗点击后也可直接进入落地页,方便用户快速进入应用查看。
  • 服务全流程展示,提升业务履约效率:用户可以在多个触达点及时关注到服务的最新进展,帮助业务实现服务的快速、高效闭环,提升业务履约效率。

支持的范围与场景

实况窗优先对满足场景准入原则和适用范围的应用开放申请。当前支持 HarmonyOS 5 及以上的操作系统版本,仅支持 Phone 和 Tablet 机型,仅支持中国境内(香港特别行政区、澳门特别行政区、中国台湾除外)。

实况窗场景准入原则

  • 该活动场景是用户非常关注,且需要反复查看或快捷操作。
  • 活动有开始和结束时间,且活动总时长较短,最长不得超过 8 小时。
  • 用户对接收到该活动的实况窗通知有明确的预期,通常为用户主动行为触发实况窗通知。
  • 需要确保展示内容对用户有足够的价值,且不可用于营销、广告场景。

实况窗支持对接的场景如下表所示:

场景类型EVENT 取值场景描述适用范围
出行打车TAXI用户线上约车后,向用户展示司机接驾等待时间、行程中的剩余距离和时间等信息。适用于网约车、出租车、拼车、顺风车等场景。
即时配送DELIVERY指配送员将餐品、商品送达到用户指定地点的业务场景,通常在较短时间内完成配送环节。适用于外卖、生鲜配送、同城配送等场景。不适用:快递物流运输进度。
航班FLIGHT用户主动关注某个航班时,向用户展示航班的关键变动,如航班开始登机、航班起飞、航班延误、航班取消、航班到达等关键场景。适用于用户通过航班出行或者主动关注某个航班进展的场景。不适用:模拟飞行、模拟航班等。
高铁/火车TRAIN用户通过高铁、火车出行,向用户展示检票口、座位号、车次信息及列车运行状态等信息。适用于高铁出行、火车出行场景。
排队QUEUE需要通过排队叫号的方式,按顺序为用户提供服务的业务场景。适用于办事大厅、医院、银行、餐饮等排队叫号能力场景。不适用:无进度排队、在线客服接入排队、文件下载排队等。
取餐PICK_UP指的是用户完成餐品/商品下单后,自行取餐或者取件的场景。适用于餐饮线下取餐提醒,包括餐品排队情况、制作进度、取餐提醒等。不适用:模拟取餐、无进度取餐等。
赛事比分SCORE展示比赛双方成绩变化情况。适用于游戏赛事、体育赛事等展示比分变化情况的场景。不适用:主播 PK 比分、象棋游戏等。
共享租赁RENT用户使用临时租赁服务时,向用户展示实时租赁时长和费用等租赁状态信息的场景。适用于共享单车、共享充电宝、停车场临时停车、汽车快充充电场景。不适用:汽车慢充、家用智能家电设备状态、共享 WIFI 等。
计时TIMER用户在某个短时间段持续的正计时或任务前的倒计时场景。适用于专注时刻、番茄时钟、抢票倒计时提醒场景,仅限于工具类应用申请(计时场景仅支持通过端侧创建与更新)。不适用:取餐计时提醒、课程提醒、事项待办、会议日程提醒、录音、翻译计时、AI 对话时长、待支付提醒等。
订阅计时SUBSCRIBE_TIMER用户主动手动订阅的演唱会售票、车票候补提醒,开售前向用户提醒开售倒计时抢购信息;用户须在开播前主动手动订阅全球官方赛事后,才可向用户展示赛事开播实况窗。适用于用户主动订阅的演唱会售票、车票候补提醒及全球官方赛事开播提醒场景。其中演唱会售票须为正规渠道公开发售的场次,车票候补须为官方平台的有效候补订单;全球官方赛事须为全球性官方顶级赛事(如四年一届),且持有国内正规直播转播版权并具备全民级关注度,无版权赛事不开放实况窗开播订阅提醒。不适用:直播订阅(含电商直播、带货直播、娱乐秀场、日常自媒体)、体育联赛常规赛/分站赛、小众地区赛事或联赛,以及电商优惠抢购订阅、优惠券/红包/活动签到等场景。
运动锻炼WORKOUT运动过程中,向用户实时展示运动的时长和进度等信息。适用于户外或室内的运动记录,如跑步、骑行。
导航NAVIGATION用户使用导航服务时,展示将要发生的路线变化。适用于步行导航、骑行导航、车辆导航。不适用:虚拟导航、游戏导航等。
打卡CHECK_IN在上下班时间点,提醒用户打卡。适用于上下班打卡场景。不适用:景区推荐/打卡、课程打卡、打卡挑战、直播打卡、居家办公打卡等。
快递EXPRESS用户存在待取件快递时,提醒用户取件。适用于快递取件场景。不适用:未接入实况窗地理围栏的快递信息推送。
进度类型PROGRESS在上传/下载文件、音视频编辑导入/导出资源等场景下展示任务完成度。适用于用户手动开启的文件上传/下载、资源导入/导出等场景下的任务进度提醒。不适用:文件在后台自动上传/下载、资源在后台自动导入/导出等。
金融交易TRADE在用户证券交易进入买五至卖五核心交易区间通过实况窗展示交易变化如价格、挂单、成交等有效信息。适用于股票、基金交易等场景下的成交进展提醒。

受限说明

通用约束限制
  • 实况窗的推送受权益管控,应用接入时,需要根据文档的指导开通推送服务权益和开通实况窗服务权益。
  • 应用在推送实况窗通知时,本地实况窗创建依赖应用进程运行,创建后支持本地更新和通过 Push Kit 更新两种方式。
  • 单个实况窗的生命周期最长不超过 8 小时,超过 8 小时后,系统会认为实况窗结束。
  • 系统会在以下情况自动调整实况窗的展示:
    • 超过 2 小时未更新:状态栏胶囊和锁屏胶囊将被隐藏,仅保留在通知中心展示;
    • 超过 4 小时未更新:系统将判定实况窗已结束,并从所有展示入口清除该实况窗。
  • 用户可以在实况窗通知展示的任何时间点对某一实况窗通知进行删除,删除后,该实况窗通知的更新将不再展示。
  • 实况窗创建和更新有流控机制:
    • 系统级流控(针对所有应用):实况通知创建每秒最多 15 次,实况通知更新每秒最多 30 次,超过频次部分被丢弃不下发。
    • 应用级流控(针对单个应用):实况通知创建每秒最多 10 次,实况通知更新每秒最多 20 次,超过频次部分被丢弃不下发。
  • 创建和更新本地实况窗场景,从 API 版本 26.0.0 开始,传入图片 PixelMap 实例大小不大于 192KB;对于 API 版本 26.0.0 之前,传入图片 PixelMap 实例大小不大于 30KB。
  • 为避免在 HarmonyOS 7.0.0 之前旧系统版本上实况窗无法显示 PixelMap 类型图片,要求在新旧系统版本上传入不同大小的图片 PixelMap 实例进行适配,请参考应用使用 API 如何在不同系统版本设备上做兼容性保护判断。
通过 Push Kit 创建和更新实况窗的约束限制
  • 通过 Push Kit 创建实况窗当前仅支持 FLIGHT、TAXI、TRAIN、DELIVERY、QUEUE、RENT、EXPRESS、CHECK_IN、TRADE、SUBSCRIBE_TIMER 场景。
  • 实况窗可以通过 Push Kit 进行更新,保证实况窗不依赖应用进程的存活。利用 Push Kit 的推送服务,实况窗能够实现生命周期内的正常更新和结束。
  • 通过 Push Kit 更新实况窗时,单个实况窗消息,出行打车与赛事比分场景每个设备每 5 分钟最多更新 30 次,每小时最多更新 180 次。其余场景每个设备每 5 分钟最多更新 10 次,每小时最多更新 60 次。超过频次部分将丢弃不下发。
基于地理位置的实况窗提醒功能约束限制
  • 从 6.1.0(23) 开始,支持基于地理位置的实况窗提醒功能。
  • 基于地理位置的实况窗提醒次数限制:单设备单应用每日可最多注册 5 次。
  • 基于地理位置的实况窗提醒注册后,有效期为 48 小时,若超时未触发,系统将自动删除已过期的基于地理位置的实况窗提醒。
  • 当满足地理位置条件,触发创建或结束实况窗后,系统将自动删除该地理位置触发条件,不会再次触发创建或结束实况窗。
  • 运动锻炼(WORKOUT)和导航(NAVIGATION)场景不支持注册基于地理位置的实况窗提醒。
  • 基于地理位置的实况窗提醒依赖 GNSS 芯片的地理围栏功能,仅在室外开阔区域才能准确识别用户进出围栏事件并触发创建或结束实况窗。
  • 若“设置 > 通知和状态栏 > 实况窗”或“设置 > 应用和元服务”中的应用实况窗开关关闭,系统将自动删除该应用已注册但未触发的基于地理位置的实况窗提醒。
  • 若“设置 > 通知和状态栏 > 实况窗”,点击页面右上方 4 个点标记,点击“个性化提醒”,页面中“基于地理位置的实况窗提醒”开关关闭,系统将自动删除所有已注册但未触发的基于地理位置的实况窗提醒。

模拟器支持情况

Live View Kit 支持模拟器,但与真机存在部分能力差异。模拟器不支持基于地理位置的实况窗提醒、实况窗雨雪天气动效背景、实况窗连续服务按钮。

适配过程

开发者需要按照如下流程完成实况窗的开发工作:

  1. 依据实况窗设计规范设计实况窗通知样式。
  2. 开发准备:在 AppGallery Connect 中申请实况窗权限。
  3. 构建本地实况窗。
  4. 通过 Push Kit 创建实况窗。
  5. 通过 Push Kit 更新实况窗。

实况窗卡片和胶囊的内容设计上,需要注意:

  • 禁止展示涉及用户隐私的信息。
  • 禁止将实况窗用于营销、广告等场景。
  • 应用发送的实况窗需遵循实况窗设计规范,不符合设计规范的方案将不被予以开通正式权限。同时若应用实况窗上线后出现违反实况窗设计规范的行为,将被视为违规。

开发准备

设置数据处理位置

若开发者要通过 Push Kit 更新实况窗,需要设置默认数据处理位置为“中国”,否则可能导致推送消息无法正常下发,从而影响通过 Push Kit 更新实况窗的功能。

设置步骤如下:

  1. 登录 AppGallery Connect,选择“开发与服务”。
  2. 在项目列表中点击需要设置数据处理位置的项目。
  3. 进入“项目设置 > 数据处理位置”页面,点击“管理”。
  4. 在“是否已启用”栏勾选“中国”,并在“是否设为默认”栏将中国设置为默认数据处理位置。
  5. 设置完成后,点击“保存”。

说明:如果设置的数据处理位置与开发者的服务器位置不一致,或者设置的数据处理位置与应用所服务的用户所在地不一致,都会导致推送消息无法下发。

开通推送服务权益

若需要通过 Push Kit 更新实况窗,开发者需要首先为项目开通“推送服务”权益。开通推送服务后,开发者可通过 Push Kit 更新实况窗,或使用 Push Token 添加调测设备调测实况窗。

开通实况窗服务权益

若需完成应用的实况窗接入与调测,开发者需预先开通实况窗服务权益。

操作步骤:

  1. 登录 AppGallery Connect,选择“开发与服务”。
  2. 在项目列表中找到您的项目,在项目下的应用列表中选择需要申请实况窗服务的应用。
  3. 进入“项目设置 > 开放能力管理”页面,点击“实况窗服务”的“申请”。
  4. 开发者可参考“申请原因”中的模板,提供申请必须的相关信息,包括场景类型、场景描述、附件,然后点击“提交”按钮。

说明:开发者可依据实况窗设计规范,通过实况窗可视化工具,完成实况窗场景节点与方案视图设计。完成设计后,请将方案归档整理并作为附件提交,以此申请实况窗使用权益。开发者将在 5 个工作日内收到实况窗服务权益申请结果。

实况窗服务权限通过后,开发者需在 AppGallery Connect 选择“证书、APP ID 和 Profile”,点击左侧树形菜单的“Profile”页签,在页面右上角点击“添加”按钮,重新生成 Profile 文件,并将其下载至本地。在“发布应用”时,将该 Profile 打包到应用包中。

构建本地实况窗

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

导入 liveViewManager

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

完整示例代码:

import { liveViewManager } from '@kit.LiveViewKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from '../LogUtil';

export class LiveViewController {
  public static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
创建实况窗

实况窗根据扩展区不同共有 5 种样式模板:进度可视化模板强调文本模板左右文本模板赛事比分模板导航模板。调用 liveViewManager.startLiveView 创建实况窗,该 API 接口传入参数为实况窗实例(liveViewManager.LiveView)。

进度可视化模板

进度可视化模板适用于打车、外卖等场景。从 6.0.2(22) 开始,实况窗卡片进度可视化模板支持显示雨、雪天气动效背景。

以下是完整的 ProgressLiveViewController 类定义,包含创建实况窗所需的全部逻辑:

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class ProgressLiveViewController {
  public async startLiveView(): Promise<boolean> {
    // 校验实况窗开关是否打开
    if (!await ProgressLiveViewController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    // 创建实况窗
    try {
      const defaultView = await ProgressLiveViewController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

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

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}

同时需要 ContextUtil 工具类用于构造点击动作的 WantAgent:

import { common, Want, WantAgent, wantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from './LogUtil';

export class ContextUtil {
  public static wantUrl: string | undefined;
  public static liveViewId: number;
  public static applicationContext: common.ApplicationContext;

  public static async buildWantAgent(page: string, liveViewId: number = -1): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: ContextUtil.applicationContext.applicationInfo.name,
          abilityName: 'EntryAbility',
          parameters: {
            page: page,
            liveViewId: liveViewId
          },
        } as Want
      ],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    try {
      const agent: WantAgent = await wantAgent.getWantAgent(wantAgentInfo);
      Logger.info('getWantAgent success! wantAgent: %{public}s', JSON.stringify(agent));
      return agent;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('getWantAgent failed! err: %{public}d %{public}s', err.code, err.message);
      throw e as Error;
    }
  }
}
强调文本模板

强调文本模板适用于取餐、排队等场景。从 6.0.2(22) 开始,实况窗卡片强调文本模板支持显示雨、雪天气动效背景。

完整示例代码:

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class PickupLiveViewController {
  public async startLiveView(): Promise<boolean> {
    if (!await PickupLiveViewController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await PickupLiveViewController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 105,
      event: 'PICK_UP', // 取餐场景
      isMute: false,
      liveViewData: {
        primary: {
          title: '餐品已备好',
          content: [
            { text: '请前往' },
            { text: ' XXX店 ', textColor: '#FF0A59F7' },
            { text: '取餐' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
            weatherInfo: {
              weatherType: liveViewManager.WeatherType.WEATHER_TYPE_HEAVY_SNOW,
              locationType: liveViewManager.WeatherLocationType.LOCATION_TYPE_LOCAL
            },
            title: '取餐码',
            content: '72988',
            underlineColor: '#FF0A59F7',
            descPic: 'coffee.png' // 扩展区右侧产品描述图
          }
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
左右文本模板

左右文本模板适用于高铁、航班等场景。从 6.0.0(20) 开始,实况窗卡片左右文本模板支持显示雨、雪天气动效背景或夕阳、赏月氛围背景。

完整示例代码:

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class FlightLiveViewController {
  public async startLiveView(): Promise<boolean> {
    if (!await FlightLiveViewController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await FlightLiveViewController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 103,
      event: 'FLIGHT', // 航班场景
      isMute: false,
      liveViewData: {
        primary: {
          title: '计划出发',
          content: [
            { text: '登机口' },
            { text: '32', textColor: '#FF0A59F7' },
            { text: ' | 座位' },
            { text: ' 17H', textColor: '#FF0A59F7' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          /**
           * 当传入实况窗卡片的背景氛围类型参数backgroundType值为赏月航班或夕阳航班时,
           * 且同时传入天气类型(WeatherInfo)为雨、雪特殊天气,卡片上优先展示天气背景,
           * 其余非特殊天气在卡片上展示赏月航班或夕阳航班背景氛围。
           */
          backgroundType: liveViewManager.BackgroundType.SYS_BACKGROUND_FLIGHT_SUNSET,
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_FLIGHT,
            weatherInfo: {
              weatherType: liveViewManager.WeatherType.WEATHER_TYPE_LIGHT_RAIN,
              locationType: liveViewManager.WeatherLocationType.LOCATION_TYPE_DESTINATION,
              highTemperature: 30,
              lowTemperature: -10
            },
            firstTitle: '09:00',
            firstContent: '上海虹桥',
            lastTitle: '14:20',
            lastContent: '汉口',
            spaceIcon: 'icon_plane.png',
            isHorizontalLineDisplayed: false,
            additionalText: '以上信息仅供参考'
          }
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
赛事比分模板

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

完整示例代码:

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class ScoreLiveViewController {
  public async startLiveView(): Promise<boolean> {
    if (!await ScoreLiveViewController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await ScoreLiveViewController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 108,
      event: 'SCORE', // 赛事比分场景
      isMute: false,
      liveViewData: {
        primary: {
          title: '第四节比赛中',
          content: [
            { text: 'XX', textColor: '#FF0A59F7' },
            { text: ' VS ' },
            { text: 'XX', textColor: '#FF0A59F7' },
            { text: ' | ' },
            { text: '小组赛 第五场', textColor: '#FF0A59F7' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_SCORE,
            hostName: '队名 A',
            hostIcon: 'score_firefox.png',
            hostScore: '110',
            guestName: '队名 B',
            guestIcon: 'score_m.png',
            guestScore: '102',
            competitionDesc: [
              { text: '●', textColor: '#FFFF0000' },
              { text: 'Q4' }
            ],
            competitionTime: '02:16',
            isHorizontalLineDisplayed: true
          }
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
导航模板

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

完整示例代码:

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class NavigationLiveViewController {
  public async startLiveView(): Promise<boolean> {
    if (!await NavigationLiveViewController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await NavigationLiveViewController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 104,
      event: 'NAVIGATION', // 导航场景
      isMute: false,
      liveViewData: {
        primary: {
          title: '178米后左转',
          content: [
            { text: '去往' },
            { text: ' xxx东路', textColor: '#FF0A59F7' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_NAVIGATION,
            currentNavigationIcon: 'arrow_left.png', // 当前导航方向
            navigationIcons: ['arrow_left.png','arrow_up.png','arrow_up.png','arrow_right.png'] // 导航方向的箭头集合
          }
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
基于地理位置的实况窗提醒

基于地理位置的实况窗提醒适用于打卡、快递等场景。从 6.1.0(23) 开始,支持注册基于地理位置延迟触发的实况窗提醒,在注册由地理围栏条件触发的实况窗后,满足以下条件可触发创建或结束实况窗:

  • 进入地理围栏。
  • 离开地理围栏。
  • 进入地理围栏并持续时间大于 geofence.delayTime(延迟触发时间)。
  • 离开地理围栏并持续时间大于 geofence.delayTime(延迟触发时间)。

构建 LiveViewController 后,在代码中初始化并调用 startLiveViewByTrigger() 方法添加由地理围栏条件触发创建的实况窗。完整示例代码(快递场景触发创建):

import { liveViewManager } from '@kit.LiveViewKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ContextUtil } from '../ContextUtil';
import { Logger } from '../LogUtil';
import { Model } from '../model';
import { GeofenceRightsUtil } from './GeofenceRightsUtil';

export class GeofenceExpressController {
  private static defaultView: liveViewManager.LiveView | undefined = undefined;
  private static trigger: liveViewManager.Trigger | undefined = undefined;
  private static underLineColor: string = '#FF0A59F7';
  private static capsuleColor: string = '#FF308977';

  public static async startLiveViewExpress(model: Model): Promise<string> {
    let checkRightsResult = await GeofenceRightsUtil.checkRights();
    if (checkRightsResult != '') {
      return checkRightsResult;
    }
    try {
      // 构建快递实况窗。
      GeofenceExpressController.defaultView = await GeofenceExpressController.buildExpressLiveView();
      // 构建实况窗提醒的触发条件
      GeofenceExpressController.trigger = await GeofenceExpressController.buildDefaultTrigger(model);
      let createResult = await GeofenceExpressController.startLiveViewByTrigger();
      if (createResult != 0) {
        return await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Create_failed').id);
      }
      return await ContextUtil.applicationContext.resourceManager
        .getStringValue($r('app.string.Create_success').id);
    } catch (e) {
      return await ContextUtil.applicationContext.resourceManager
        .getStringValue($r('app.string.Create_failed').id);
    }
  }

  private static async startLiveViewByTrigger(): Promise<number> {
    if (!GeofenceExpressController.defaultView || !GeofenceExpressController.trigger) {
      Logger.warn('startLiveViewByTrigger, buildDefaultView or buildDefaultTrigger failed.')
      return -1;
    }
    // 注册由地理围栏条件延迟触发创建的实况窗
    try {
      Logger.info('Request startLiveViewByTrigger req liveView: %{public}s, trigger: %{public}s',
        JSON.stringify(GeofenceExpressController.defaultView), JSON.stringify(GeofenceExpressController.trigger));
      const result = await liveViewManager.startLiveViewByTrigger(GeofenceExpressController.defaultView,
        GeofenceExpressController.trigger);
      Logger.info('Request startLiveViewByTrigger result: %{public}s', JSON.stringify(result));
      return result.resultCode;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return -1;
    }
  }

  private static async buildExpressLiveView(): Promise<liveViewManager.LiveView | undefined> {
    try {
      return {
        id: 11, // 实况窗ID,开发者生成。
        event: 'EXPRESS', // 实况窗的应用场景。EXPRESS:快递。
        sequence: 1, // 序列号
        isMute: false,
        liveViewData: {
          primary: {
            title: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Express_title')
              .id),
            content: [
              {
                text: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Express_content')
                  .id),
              }
            ],
            keepTime: 0,
            clickAction: await ContextUtil.buildWantAgent('Geofence'),
            extensionData: {
              type: liveViewManager.ExtensionType.EXTENSION_TYPE_ICON,
              pic: 'express.png',
              clickAction: await ContextUtil.buildWantAgent('Geofence', 11)
            },
            layoutData: {
              layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
              title: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Express_layoutData_title')
                .id),
              content: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Express_layoutData_content')
                .id),
              underlineColor: GeofenceExpressController.underLineColor,
              descPic: 'pick.png',
            },
          },
          capsule: {
            type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
            status: 1,
            icon: 'pick.png',
            backgroundColor: GeofenceExpressController.capsuleColor,
            title: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Express_layoutData_title')
              .id),
            content: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Express_layoutData_content')
              .id),
          }
        }
      }
    } catch (e) {
      Logger.error('buildDefaultView failed:' + JSON.stringify(e))
      return undefined;
    }
  }

  private static async buildDefaultTrigger(model: Model): Promise<liveViewManager.Trigger | undefined> {
    try {
      return {
        // 构造实况窗提醒的地理围栏触发条件。
        type: liveViewManager.TriggerType.TRIGGER_TYPE_GEOFENCE,
        displayTime: 900,
        condition: {
          // 地理围栏触发条件:设备进入坐标点500米范围内。
          longitude: model.longitude,
          latitude: model.latitude,
          coordinateSystemType: liveViewManager.CoordinateSystemType.COORDINATE_TYPE_GCJ02,
          monitorEvent: liveViewManager.MonitorEvent.MONITOR_TYPE_ENTRY,
          radius: 500,
          delayTime: 0
        }
      }
    } catch (e) {
      Logger.error('buildDefaultTrigger failed:' + JSON.stringify(e))
      return undefined;
    }
  }
}

同时需要 GeofenceRightsUtil 工具类检查权限:

import { liveViewManager } from '@kit.LiveViewKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ContextUtil } from '../ContextUtil';
import { Logger } from '../LogUtil';
import { geoLocationManager } from '@kit.LocationKit';

export class GeofenceRightsUtil {
  // 检查权限
  public static async checkRights(): Promise<string> {
    try {
      // 校验实况窗开关是否打开
      if (!await GeofenceRightsUtil.isLiveViewEnabled()) {
        Logger.warn('checkRights, 实况开关未开启.')
        return await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Live_view_enabled').id);
      }
      // 校验实况窗地理围栏开关是否打开
      if (!await GeofenceRightsUtil.isGeofenceTriggerEnabled()) {
        Logger.warn('checkRights, 地理围栏开关未开启.')
        return await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Live_view_geofence_enabled').id);
      }
      // 校验GPS开关是否打开
      if (!GeofenceRightsUtil.isLocationEnabled()) {
        Logger.warn('checkRights, GPS 开关未开启.')
        return await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Gps_enabled').id);
      }
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('checkRights error: %{public}d %{public}s', err.code, err.message);
      return await ContextUtil.applicationContext.resourceManager
        .getStringValue($r('app.string.Create_success').id);
    }
    return '';
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('isLiveViewEnabled result: %{public}s', result);
    return result;
  }

  private static async isGeofenceTriggerEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isGeofenceTriggerEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('isGeofenceTriggerEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('isGeofenceTriggerEnabled result: %{public}s', result);
    return result;
  }

  private static isLocationEnabled(): boolean {
    let result: boolean = false;
    try {
      result = geoLocationManager.isLocationEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('isLocationEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('isLocationEnabled result: %{public}s', result);
    return result;
  }
}

调用 liveViewManager.stopLiveViewByTrigger() 方法可以添加由地理围栏条件触发结束的实况窗,示例可参考官方文档中航班场景结束的代码,结构与上述类似,只是 monitorEvent 使用 MONITOR_TYPE_LEAVE

实况胶囊

胶囊形态各模板参数固定,与创建实况窗时的模板类型无关。可创建的胶囊类型有:文本胶囊、计时器胶囊、进度胶囊。若开发者创建实况窗时还想同步创建实况窗胶囊,则需在 liveViewManager.LiveView 结构体中携带胶囊所需的参数 liveViewData.capsule

完整示例代码(出行打车场景的文本胶囊):

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class LiveViewCapsuleController {
  public async startLiveView(): Promise<boolean> {
    if (!await LiveViewCapsuleController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await LiveViewCapsuleController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 101,
      event: 'TAXI', // 出行打车场景
      isMute: false,
      liveViewData: {
        primary: {
          title: '司机预计5分钟后到达',
          content: [
            { text: '白' },
            { text: ' | ' },
            { text: '沪AXXXXXX', textColor: '#FF0A59F7' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 30,
            color: '#ff0959F8',
            backgroundColor: '#ffc9d7e4',
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_UP,
            indicatorIcon: 'taxi-transport-icon.png',
            lineType: liveViewManager.LineType.LINE_TYPE_NORMAL_SOLID_LINE,
            nodeIcons: ['icon_order.png', 'icon_finish.png']
          }
        },
        // 实况胶囊相关参数
        capsule: {
          type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
          status: 1,
          icon: 'capsule_taxi.png',
          backgroundColor: '#ff0959F8',
          title: '已接单',
          content: '约3分钟'
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
小折叠外屏实况窗

外屏实况窗适用于在小折叠屏的外屏显示实况窗的简要信息。若开发者创建实况窗时需要同步创建,则需在 liveViewManager.LiveView 结构体中携带外屏所需的参数 liveViewData.external

完整示例代码(航班场景):

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class LiveViewExternalController {
  public async startLiveView(): Promise<boolean> {
    if (!await LiveViewExternalController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await LiveViewExternalController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 102,
      event: 'FLIGHT', // 航班场景
      isMute: false,
      liveViewData: {
        primary: {
          title: '航班 XXX 已值机',
          content: [
            { text: '登机口', },
            { text: '27 17:45', textColor: '#FFFF9C4F' },
            { text: '开始登机' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_FLIGHT,
            firstTitle: '18:15',
            firstContent: '上海',
            lastTitle: '20:30',
            lastContent: '成都',
            spaceIcon: 'icon_plane.png',
            isHorizontalLineDisplayed: true,
            additionalText: '以上信息仅供参考'
          }
        },
        external: {
          title: '已值机',
          content: [
            { text: '登机口' },
            { text: '27\n', textColor: '#FFFF9C4F' },
            { text: '17:45', textColor: '#FFFF9C4F' },
            { text: '开始登机' }
          ],
          type: liveViewManager.ExternalType.BACKGROUND_PICTURE,
          backgroundPicture: 'airplane.png'
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
实况窗计时器

实况窗计时器适用于排队、抢票等场景。开发者若需要使用实况窗计时器,则需在 liveViewManager.LiveView 结构体中配置 timer 字段,并在当前支持的字段中使用占位符 ${placeholder.timer}

完整示例代码(排队场景):

import { liveViewManager } from '@kit.LiveViewKit';
import { Logger } from '../LogUtil';
import { ContextUtil } from '../ContextUtil';
import { BusinessError } from '@kit.BasicServicesKit';

export class QueueLiveViewController {
  public async startLiveView(): Promise<boolean> {
    if (!await QueueLiveViewController.isLiveViewEnabled()) {
      Logger.warn('startLiveView, live view is disabled.');
      return false;
    }
    try {
      const defaultView = await QueueLiveViewController.buildDefaultView();
      if (!defaultView) {
        Logger.warn('buildDefaultView Failed.');
        return false;
      }
      Logger.info('Request startLiveView req: %{public}s', JSON.stringify(defaultView));
      const result = await liveViewManager.startLiveView(defaultView);
      Logger.info('Request startLiveView result: %{public}s', JSON.stringify(result));
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request startLiveView error: %{public}d %{public}s', err.code, err.message);
      return false;
    }
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      id: 107,
      event: 'QUEUE', // 排队场景
      isMute: false,
      timer: {
        time: 300000,
        isCountdown: false,
        isPaused: false
      },
      liveViewData: {
        primary: {
          title: '大桌 4 人等位  32 桌',
          content: [
            { text: '已等待 ' },
            { text: ' ${placeholder.timer}', textColor: '#ff10c1f7' },
            { text: '分钟 | 预计还需>30 分钟' }
          ],
          keepTime: 0,
          clickAction: await ContextUtil.buildWantAgent('GuideCode'),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 40,
            color: '#FF317AF7',
            backgroundColor: '#f7819ae0',
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_UNDISPLAYED,
            lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
            nodeIcons: ['icon_order.png', 'icon_finish.png']
          }
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    Logger.info('Request isLiveViewEnabled result: %{public}s', result);
    return result;
  }
}
点击实况窗动作

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

  • 点击实况窗的默认动作:在 liveViewManager.LiveView 结构体中携带 liveViewData.primary.clickAction 字段。
  • 点击辅助区的跳转动作:在 liveViewManager.LiveView 结构体中携带 liveViewData.primary.extensionData.clickAction 字段。
本地更新和结束实况窗

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

完整示例代码(取餐场景的更新与结束):

import { liveViewManager } from '@kit.LiveViewKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ContextUtil } from './ContextUtil';
import { Logger } from './LogUtil';

export class PickLiveViewController {
  private static defaultView: liveViewManager.LiveView | undefined = undefined;
  private static contentColor: string = '#FF0A59F7';
  private static underLineColor: string = '#FF0A59F7';
  private static capsuleColor: string = '#FF308977';

  public async startLiveView(): Promise<boolean> {
    if (!await PickLiveViewController.isLiveViewEnabled()) {
      return false;
    }
    PickLiveViewController.defaultView = await PickLiveViewController.buildDefaultView();
    if (!PickLiveViewController.defaultView) {
      return false;
    }
    try {
      const result = await liveViewManager.startLiveView(PickLiveViewController.defaultView);
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      return false;
    }
    return true;
  }

  public async updateLiveView(): Promise<boolean> {
    try {
      if (!PickLiveViewController.defaultView) {
        return false;
      }
      // 修改实况窗内容
      PickLiveViewController.defaultView.isMute = false;
      PickLiveViewController.defaultView.liveViewData.primary.title =
        await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Pick_wait_primary_title').id);
      PickLiveViewController.defaultView.liveViewData.primary.content = [
        {
          text: await ContextUtil.applicationContext.resourceManager
            .getStringValue($r('app.string.Pick_wait_primary_content').id)
        }
      ];
      PickLiveViewController.defaultView.liveViewData.primary.layoutData = {
        layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
        title: await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Pick_wait_layout_title').id),
        content: await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Pick_wait_layout_content').id),
        underlineColor: PickLiveViewController.underLineColor,
        descPic: 'coffee.png'
      };
      PickLiveViewController.defaultView.liveViewData.capsule = {
        type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
        status: 1,
        icon: 'capsule_to_pick.png',
        backgroundColor: PickLiveViewController.capsuleColor,
        title: await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Pick_wait_capsule_title').id)
      };

      const result = await liveViewManager.updateLiveView(PickLiveViewController.defaultView);
      return true;
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      return false;
    }
  }

  public async stopLiveView(): Promise<void> {
    try {
      if (!await PickLiveViewController.isLiveViewEnabled() || !PickLiveViewController.defaultView) {
        return;
      }
      PickLiveViewController.defaultView.liveViewData.primary.title =
        await ContextUtil.applicationContext.resourceManager
          .getStringValue($r('app.string.Pick_finished_primary_title').id);
      PickLiveViewController.defaultView.liveViewData.primary.content = [
        {
          text: await ContextUtil.applicationContext.resourceManager
            .getStringValue($r('app.string.Pick_finished_primary_content1').id)
        },
        {
          text: await ContextUtil.applicationContext.resourceManager
            .getStringValue($r('app.string.Pick_finished_primary_content2').id)
        }
      ];
      PickLiveViewController.defaultView.liveViewData.primary.layoutData = {
        layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
        title: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Pick_finished_primary_layout_title')
          .id),
        content: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Pick_finished_primary_layout_content')
          .id),
        underlineColor: PickLiveViewController.underLineColor,
        descPic: 'icon_store.png'
      };
      const result = await liveViewManager.stopLiveView(PickLiveViewController.defaultView);
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request stopLiveView error: %{public}d %{public}s', err.code, err.message);
    }
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    let result: boolean = false;
    try {
      result = await liveViewManager.isLiveViewEnabled();
    } catch (e) {
      const err: BusinessError = e as BusinessError;
      Logger.error('Request isLiveViewEnabled error: %{public}d %{public}s', err.code, err.message);
    }
    return result;
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView | undefined> {
    try {
      return {
        id: 10,
        event: 'PICK_UP',
        liveViewData: {
          primary: {
            title: await ContextUtil.applicationContext.resourceManager
              .getStringValue($r('app.string.Delivery_default_primary_title').id),
            content: [
              {
                text: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Delivery_default_primary_content1')
                  .id),
                textColor: PickLiveViewController.contentColor
              },
              {
                text: ' ' +
                  await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Delivery_default_primary_content2')
                    .id)
              }
            ],
            keepTime: 15,
            clickAction: await ContextUtil.buildWantAgent('PickUp'),
            extensionData: {
              type: liveViewManager.ExtensionType.EXTENSION_TYPE_ICON,
              pic: 'icon_merchant.png',
              clickAction: await ContextUtil.buildWantAgent('PickUp')
            },
            layoutData: {
              layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
              title: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Delivery_default_layout_title')
                .id),
              content: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Delivery_default_layout_content')
                .id),
              underlineColor: PickLiveViewController.underLineColor,
              descPic: 'coffee.png'
            },
          },
          capsule: {
            type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
            status: 1,
            icon: 'capsule_purse.png',
            backgroundColor: PickLiveViewController.capsuleColor,
            title: await ContextUtil.applicationContext.resourceManager.getStringValue($r('app.string.Delivery_default_capsule_title')
              .id)
          }
        }
      }
    } catch (e) {
      return undefined;
    }
  }
}

通过 Push Kit 更新实况窗

本地实况窗的更新依赖于应用进程的存活,为了让实况窗在生命周期内正常完成更新和结束,推荐开发者使用 Push Kit 实时更新实况窗状态。

流程

  1. 使用 Push Kit,获取 Push Token。
  2. 使用 Live View Kit 创建实况窗成功后,开发者需要将实况窗 id、pushToken、实况窗场景 event 以及业务服务的相关的状态属性保存到业务服务端。
  3. 当业务服务的用户订单状态发生变化时,通过 Push Kit 通道推送更新消息,更新/结束实况窗。

支持网络图片下载:从 26.0.0 开始,实况窗支持通过 Push Kit 下载网络图片,限制条件包括:图片大小不大于 512KB;文件格式为 jpg、jpeg、png、bmp、webp;仅支持 HTTPS 协议;仅基础模板、进度可视化模板、强调文本模板、左右文本模板、赛事比分模板、胶囊模板中的指定位置支持网络图片下载。

接入联调测试

若开发者需要在设备上调试、验证实况窗,可通过“调测设备管理”入口,添加设备进行调测。添加到调测名单中的设备,不做本地构建实况窗权限的校验。调测设备管理能力可用于应用实况窗场景上线前的用户验证。

约束和限制

  • 添加的调测设备管理名单数据,系统将在 24 小时内处理并生效,调测权限有效期与 Push Token 有效期保持一致。
  • 调测设备管理需根据 Push Token 添加调测设备,每个应用可添加的设备上限为 100。

添加调测设备步骤

  1. 登录 AppGallery Connect 网站,点击“开发与服务”,在项目列表中找到开发者的项目,通过“增长 > 推送服务 > 配置”导航到“配置”页签。
  2. 选择开发者的应用,点击实况窗-调测设备管理,根据 Push Token 添加调测设备后即可进行接入调测。

申请实况窗正式权限

当开发者已对调测设备的实况窗业务进行了充分的联调测试,确认设计方案和功能体验均符合《实况窗设计规范》,可提交申请正式权限。提交后实况窗将对开发者的方案设计、功能体验进行评审与验收。开发者将会在 7 个工作日内收到评审结果。

操作步骤

  1. 登录 AppGallery Connect,选择“开发与服务”。
  2. 在项目列表中找到需要开通实况窗的项目。
  3. 通过“增长 > 推送服务 > 配置”导航到“配置”页签,选择需要开通实况窗的应用,并点击“实况窗”的“申请”。
  4. 点击开通实况窗权限,进入实况窗介绍页面,点击“立即申请”。
  5. 若开发者的应用月活数大于等于 1000 且为已上架应用,可点击“应用场景”列表中各场景的“申请”按钮,按需申请开通实况窗权益。
  6. 按要求填写场景的描述信息、场景接入方案和备注信息后提交申请,等待审批结果。

实况窗权益申请填写要求

  • 场景描述:需包含接入场景的描述、消息的展示时机基本说明、展示的主要节点。开发者填写的内容将用于客服答疑等场景,请尽可能详细地描述。若开发者的应用内支持使用其他应用的小程序,需明确说明本次申请的场景使用范围是否涉及到其他应用的小程序。若涉及,开发者需确保不会出现同一个任务多端推送实况窗的体验,并附上与小程序客户端的沟通对齐证明和策略变更预案。
  • 接入方案:请将应用已验收通过的接入方案现网效果截图(含每个状态节点的卡片、胶囊、锁屏效果、展示时机说明)、主要特殊场景方案效果放在同一张图片中,图片大小需控制在 3M 内。实况窗接入方案需满足《实况窗设计规范》中的要求,开发者可按照模板进行设计。

申请前自验:应用在申请实况窗权限时,需对应用当前实况窗的效果和体验进行自检验收,并在申请时将自检项结果通过备注说明,如某项内容已完成自检,可在“[ ]”中打√。

自检项包括:

  • 确认上传的截图满足实况窗设计规范要求。
  • 每个创建的实况窗活动均已添加结束事件。
  • 确认实况窗与应用内任务进度与信息一致。
  • 确认同一实时任务不存在多个实况窗。
  • 确认已考虑并提供主要特殊场景的方案。
  • 已经完成实况窗场景测试,满足上线要求。
  • 认可实况窗的管理规范,若出现不符合设计规范或者违背场景准入要求,同意华为对相关场景[场景名称]权限进行收回。

注意:开通正式权益涉及方案评审与测试验收,方案评审阶段通过后,须开发者配合测试验收(如提供验收方式和验收版本)。整个流程周期约 15 个工作日,请留意 AppGallery Connect 平台申请结果或邮箱。

常见问题与注意事项

更新实况窗被频控的问题
  • 通过 Push Kit 更新实况窗时,单个实况窗消息,出行打车与赛事比分场景每个设备每 5 分钟最多更新 30 次,每小时最多更新 180 次;其余场景每个设备每 5 分钟最多更新 10 次,每小时最多更新 60 次。超过频次部分将丢弃不下发。
  • 实况窗创建和更新有流控机制:系统级流控(创建每秒最多 15 次,更新每秒最多 30 次);应用级流控(创建每秒最多 10 次,更新每秒最多 20 次),超过频次部分被丢弃不下发。
三方开发框架接入的问题

实况窗随 HarmonyOS 整体框架支持三方平台,HarmonyOS 为常见的三方开发框架提供了接入指南:React Native、Flutter、Uni-app。

关于实况窗生命周期的问题
  • App 进程结束时关闭构建的实况窗:可以在 UIAbility 生命周期的 onDestroy() 方法内调用 liveViewManager.stopLiveView 方法,设置参数 PrimaryData 实例的 keepTime 值为 0,即可实现立即关闭实况窗。若因 App 进程异常终止场景导致无法调用到应用的 onDestroy() 方法,则实况窗不会消失。从 26.0.0 版本开始,新增支持在创建实况窗时,应用可通过指定实况生命周期模式实现自动关闭实况窗:设置参数 LiveView 实例的 lifeCycleMode 值为 AUTO_STOP_WHEN_APP_TERMINATE,即可在应用进程结束后自动关闭实况窗。
  • 通过指定实况窗最长存活时间实现自动关闭:从 6.1.1(24) 版本起,应用可在创建时通过设置 PrimaryData 实例的 aliveTime 属性来实现自动关闭功能。
  • 本地更新获取实况窗实例及清除后的限制:本地更新实况窗时,可以通过 liveViewManager.getActiveLiveView 函数获取活动的 LiveView 实例。如果实况窗被 notificationManager.cancelnotificationManager.cancelAll 清除后,无论是 Live View Kit 还是 Push Kit,无法再次通过该 id 更新或结束实况窗。再次创建该 id 的实况窗时,Live View Kit 可以立即再次创建,Push Kit 在 12 小时内无法通过该 id 再次创建实况窗。
关于实况窗模板使用的问题
  • 采用进度可视化模板并且 indicatorTypeINDICATOR_TYPE_OVERLAY 时,图片较宽,无法完全覆盖进度条:当 indicatorType=INDICATOR_TYPE_OVERLAY 时,图标区域为 64*56vp,图片较宽时会按比例进行缩放。应用需要自己修改图片大小和样式来达到想要的效果。
  • 如何修改“实况窗左上角图标”:除导航模板通过 currentNavigationIcon 设置左上角图标外,其他模板不支持修改实况窗左上角图标,默认展示为应用 Logo 图标。
关于实况窗数量约束的问题
  • 创建实况时的 id 约束:唯一性,应用可以一次性创建多个实况窗,需要保证每个实况窗 id 唯一,同一 id 在同一时刻只能创建一个实况窗。实况窗 id 复用限制:当实况窗结束后,Live View Kit 可以立即通过该 id 再次创建,Push Kit 在 12 小时内不允许重复使用该 id 创建实况窗。
  • 展示实况窗时的交互约束:在通知中心通过滑动最多展示 24 条实况窗。通过点击胶囊弹出的实况窗列表,无法滑动,只能展示 5 条实况窗。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。