【鸿蒙开发实战】HarmonyOS 实况窗教程:上传进度实时展示

HarmonyOS 实况窗教程:上传进度实时展示

一、概述

实况窗(Live View)是 HarmonyOS 提供的系统级实时信息展示能力,可将应用的进行中任务以卡片形式展示在实况窗、通知中心和锁屏上。与普通通知不同,实况窗支持实时更新进度点击回跳应用

本教程以「云星图」项目的图片上传场景为例,讲解:

  1. 实况窗的创建与进度更新 -- 进度条布局 + 胶囊状态 + 序列号递增
  2. keepAlive 续期机制 -- 防止 keepTime 到期导致实况窗消失
  3. 与后台长时任务联动 -- 后台上传时不被系统杀进程
  4. WantAgent 点击回跳 -- 用户点击实况窗回到应用
  5. 生命周期管理 -- 初始化/更新/失败/关闭的完整闭环

整体架构如下:
#mermaid-svg-OcmUTJQwUV2cPuHA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OcmUTJQwUV2cPuHA .error-icon{fill:#552222;}#mermaid-svg-OcmUTJQwUV2cPuHA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OcmUTJQwUV2cPuHA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OcmUTJQwUV2cPuHA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OcmUTJQwUV2cPuHA .marker.cross{stroke:#333333;}#mermaid-svg-OcmUTJQwUV2cPuHA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OcmUTJQwUV2cPuHA p{margin:0;}#mermaid-svg-OcmUTJQwUV2cPuHA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA .cluster-label text{fill:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA .cluster-label span{color:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA .cluster-label span p{background-color:transparent;}#mermaid-svg-OcmUTJQwUV2cPuHA .label text,#mermaid-svg-OcmUTJQwUV2cPuHA span{fill:#333;color:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA .node rect,#mermaid-svg-OcmUTJQwUV2cPuHA .node circle,#mermaid-svg-OcmUTJQwUV2cPuHA .node ellipse,#mermaid-svg-OcmUTJQwUV2cPuHA .node polygon,#mermaid-svg-OcmUTJQwUV2cPuHA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OcmUTJQwUV2cPuHA .rough-node .label text,#mermaid-svg-OcmUTJQwUV2cPuHA .node .label text,#mermaid-svg-OcmUTJQwUV2cPuHA .image-shape .label,#mermaid-svg-OcmUTJQwUV2cPuHA .icon-shape .label{text-anchor:middle;}#mermaid-svg-OcmUTJQwUV2cPuHA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OcmUTJQwUV2cPuHA .rough-node .label,#mermaid-svg-OcmUTJQwUV2cPuHA .node .label,#mermaid-svg-OcmUTJQwUV2cPuHA .image-shape .label,#mermaid-svg-OcmUTJQwUV2cPuHA .icon-shape .label{text-align:center;}#mermaid-svg-OcmUTJQwUV2cPuHA .node.clickable{cursor:pointer;}#mermaid-svg-OcmUTJQwUV2cPuHA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OcmUTJQwUV2cPuHA .arrowheadPath{fill:#333333;}#mermaid-svg-OcmUTJQwUV2cPuHA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OcmUTJQwUV2cPuHA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OcmUTJQwUV2cPuHA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OcmUTJQwUV2cPuHA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OcmUTJQwUV2cPuHA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OcmUTJQwUV2cPuHA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OcmUTJQwUV2cPuHA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OcmUTJQwUV2cPuHA .cluster text{fill:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA .cluster span{color:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OcmUTJQwUV2cPuHA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OcmUTJQwUV2cPuHA rect.text{fill:none;stroke-width:0;}#mermaid-svg-OcmUTJQwUV2cPuHA .icon-shape,#mermaid-svg-OcmUTJQwUV2cPuHA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OcmUTJQwUV2cPuHA .icon-shape p,#mermaid-svg-OcmUTJQwUV2cPuHA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OcmUTJQwUV2cPuHA .icon-shape .label rect,#mermaid-svg-OcmUTJQwUV2cPuHA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OcmUTJQwUV2cPuHA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OcmUTJQwUV2cPuHA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OcmUTJQwUV2cPuHA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 系统层
StarMapLiveViewController
应用层
EntryAbility.onCreate

init() 缓存 WantAgent
NormalUpLoad.doBatchUpload

上传流程
EntryAbility.onForeground

上传完成则关闭
EntryAbility.onDestroy

关闭实况窗
init

构建 WantAgent
startUpload

创建实况窗 + 启动续期
updateProgress

更新进度/文案/胶囊
showFailed

切换失败样式
keepAlive

每 10s 续期
stop

keepTime=0 关闭
liveViewManager

startLiveView / updateLiveView / stopLiveView
backgroundTaskManager

startBackgroundRunning / stopBackgroundRunning
wantAgent

getWantAgent

二、前置准备

2.1 AGC 开通实况窗权益

必须在 AGC(AppGallery Connect)开通 Live View Kit 权益后才能使用 。未开通时调用 startLiveView 会报错(错误码 67108900),且上架后功能会失效。

官方参考文档: 开发准备-Live View Kit(实况窗服务)-应用服务 - 华为HarmonyOS开发者

开通步骤:

  1. 登录 AppGallery Connect 后台
  2. 进入「我的项目」-> 选择目标项目 -> 「HarmonyOS 应用」
  3. 在「API 管理」或「增长」->「实况窗」中找到 Live View Kit
  4. 点击「开通」并提交场景说明文档(描述使用实况窗的业务场景)
  5. 等待审核(约 3 个工作日)

#mermaid-svg-q0AJR8gPINFy53Z2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q0AJR8gPINFy53Z2 .error-icon{fill:#552222;}#mermaid-svg-q0AJR8gPINFy53Z2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q0AJR8gPINFy53Z2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q0AJR8gPINFy53Z2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q0AJR8gPINFy53Z2 .marker.cross{stroke:#333333;}#mermaid-svg-q0AJR8gPINFy53Z2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q0AJR8gPINFy53Z2 p{margin:0;}#mermaid-svg-q0AJR8gPINFy53Z2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 .cluster-label text{fill:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 .cluster-label span{color:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 .cluster-label span p{background-color:transparent;}#mermaid-svg-q0AJR8gPINFy53Z2 .label text,#mermaid-svg-q0AJR8gPINFy53Z2 span{fill:#333;color:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 .node rect,#mermaid-svg-q0AJR8gPINFy53Z2 .node circle,#mermaid-svg-q0AJR8gPINFy53Z2 .node ellipse,#mermaid-svg-q0AJR8gPINFy53Z2 .node polygon,#mermaid-svg-q0AJR8gPINFy53Z2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q0AJR8gPINFy53Z2 .rough-node .label text,#mermaid-svg-q0AJR8gPINFy53Z2 .node .label text,#mermaid-svg-q0AJR8gPINFy53Z2 .image-shape .label,#mermaid-svg-q0AJR8gPINFy53Z2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-q0AJR8gPINFy53Z2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q0AJR8gPINFy53Z2 .rough-node .label,#mermaid-svg-q0AJR8gPINFy53Z2 .node .label,#mermaid-svg-q0AJR8gPINFy53Z2 .image-shape .label,#mermaid-svg-q0AJR8gPINFy53Z2 .icon-shape .label{text-align:center;}#mermaid-svg-q0AJR8gPINFy53Z2 .node.clickable{cursor:pointer;}#mermaid-svg-q0AJR8gPINFy53Z2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q0AJR8gPINFy53Z2 .arrowheadPath{fill:#333333;}#mermaid-svg-q0AJR8gPINFy53Z2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q0AJR8gPINFy53Z2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q0AJR8gPINFy53Z2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q0AJR8gPINFy53Z2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q0AJR8gPINFy53Z2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q0AJR8gPINFy53Z2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q0AJR8gPINFy53Z2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q0AJR8gPINFy53Z2 .cluster text{fill:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 .cluster span{color:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q0AJR8gPINFy53Z2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q0AJR8gPINFy53Z2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-q0AJR8gPINFy53Z2 .icon-shape,#mermaid-svg-q0AJR8gPINFy53Z2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q0AJR8gPINFy53Z2 .icon-shape p,#mermaid-svg-q0AJR8gPINFy53Z2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q0AJR8gPINFy53Z2 .icon-shape .label rect,#mermaid-svg-q0AJR8gPINFy53Z2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q0AJR8gPINFy53Z2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q0AJR8gPINFy53Z2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q0AJR8gPINFy53Z2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

登录 AGC 后台
选择目标项目
找到 Live View Kit
提交场景说明文档
审核通过?
权益开通成功

可调用 startLiveView
修改文档重新提交
开发阶段可申请

设备白名单调试

调试白名单: 开发阶段如果权益尚未正式开通,可以在 AGC 中申请将测试设备加入白名单,白名单设备上可直接调试实况窗功能。

Push Token 获取: 实况窗底层依赖 Push Kit 基础设施,应用启动时需获取 Push Token,否则 startLiveView 可能无法正常工作。本项目已在 EntryAbility.onCreate 中完成:

typescript 复制代码
import { pushService } from '@kit.PushKit';

// EntryAbility.onCreate 中获取 Push Token
pushService.getToken().then((token: string) => {
  hilog.info(DOMAIN, 'testTag', 'Push Token: %{public}s', token);
}).catch((err: BusinessError) => {
  hilog.error(DOMAIN, 'testTag', 'Failed to get push token: %{public}d %{public}s', err.code, err.message);
});

参考文档: 实况窗准备工作

2.2 依赖导入

typescript 复制代码
import { liveViewManager } from '@kit.LiveViewKit';
import { common, Want, WantAgent, wantAgent } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';

2.3 module.json5 配置

实况窗在 module.json5 中不需要声明 requestPermissions(AGC 权益是平台级开通,不在 module.json5 中配置,申请通过后需要手动签名)。但如果实况窗展示的是后台任务进度(如本教程的上传场景),需要配置后台长时任务:

json5 复制代码
{
  "module": {
    "requestPermissions": [
      {
        // 后台长时任务必需权限
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
      }
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        // 声明后台模式为数据传输
        "backgroundModes": ["dataTransfer"]
      }
    ]
  }
}

配置说明:

配置项 作用
KEEP_BACKGROUND_RUNNING 权限 允许应用在后台运行长时任务
backgroundModes: ["dataTransfer"] 声明后台任务类型为数据传输,系统据此决定保活策略

2.4 LiveView 数据结构

实况窗的核心数据结构 LiveView 包含主体区域(primary)和胶囊区域(capsule):
#mermaid-svg-lxcGgXRA6R0uIy1f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lxcGgXRA6R0uIy1f .error-icon{fill:#552222;}#mermaid-svg-lxcGgXRA6R0uIy1f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lxcGgXRA6R0uIy1f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lxcGgXRA6R0uIy1f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lxcGgXRA6R0uIy1f .marker.cross{stroke:#333333;}#mermaid-svg-lxcGgXRA6R0uIy1f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lxcGgXRA6R0uIy1f p{margin:0;}#mermaid-svg-lxcGgXRA6R0uIy1f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f .cluster-label text{fill:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f .cluster-label span{color:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f .cluster-label span p{background-color:transparent;}#mermaid-svg-lxcGgXRA6R0uIy1f .label text,#mermaid-svg-lxcGgXRA6R0uIy1f span{fill:#333;color:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f .node rect,#mermaid-svg-lxcGgXRA6R0uIy1f .node circle,#mermaid-svg-lxcGgXRA6R0uIy1f .node ellipse,#mermaid-svg-lxcGgXRA6R0uIy1f .node polygon,#mermaid-svg-lxcGgXRA6R0uIy1f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lxcGgXRA6R0uIy1f .rough-node .label text,#mermaid-svg-lxcGgXRA6R0uIy1f .node .label text,#mermaid-svg-lxcGgXRA6R0uIy1f .image-shape .label,#mermaid-svg-lxcGgXRA6R0uIy1f .icon-shape .label{text-anchor:middle;}#mermaid-svg-lxcGgXRA6R0uIy1f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lxcGgXRA6R0uIy1f .rough-node .label,#mermaid-svg-lxcGgXRA6R0uIy1f .node .label,#mermaid-svg-lxcGgXRA6R0uIy1f .image-shape .label,#mermaid-svg-lxcGgXRA6R0uIy1f .icon-shape .label{text-align:center;}#mermaid-svg-lxcGgXRA6R0uIy1f .node.clickable{cursor:pointer;}#mermaid-svg-lxcGgXRA6R0uIy1f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lxcGgXRA6R0uIy1f .arrowheadPath{fill:#333333;}#mermaid-svg-lxcGgXRA6R0uIy1f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lxcGgXRA6R0uIy1f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lxcGgXRA6R0uIy1f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lxcGgXRA6R0uIy1f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lxcGgXRA6R0uIy1f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lxcGgXRA6R0uIy1f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lxcGgXRA6R0uIy1f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lxcGgXRA6R0uIy1f .cluster text{fill:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f .cluster span{color:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lxcGgXRA6R0uIy1f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lxcGgXRA6R0uIy1f rect.text{fill:none;stroke-width:0;}#mermaid-svg-lxcGgXRA6R0uIy1f .icon-shape,#mermaid-svg-lxcGgXRA6R0uIy1f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lxcGgXRA6R0uIy1f .icon-shape p,#mermaid-svg-lxcGgXRA6R0uIy1f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lxcGgXRA6R0uIy1f .icon-shape .label rect,#mermaid-svg-lxcGgXRA6R0uIy1f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lxcGgXRA6R0uIy1f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lxcGgXRA6R0uIy1f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lxcGgXRA6R0uIy1f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LiveView
id: 实况窗唯一标识
event: 事件类型

'DELIVERY'
sequence: 序列号

每次更新必须递增
isMute: 是否静默
liveViewData
primary(主体区域)
capsule(胶囊区域)
title: 标题
content: 内容文案数组
keepTime: 存活秒数
clickAction: 点击回跳 WantAgent
layoutData: 布局数据

进度条/图标等
type: 胶囊类型
icon: 胶囊图标
content: 胶囊文案

三、封装实况窗控制器

将实况窗的所有操作封装为静态管理器 StarMapLiveViewController,供上传流程调用。

3.1 初始化:缓存 WantAgent

WantAgent 用于实况窗的点击回跳,在应用启动时提前构建并缓存,避免上传时异步等待:

typescript 复制代码
export class StarMapLiveViewController {
  private static liveView: liveViewManager.LiveView | undefined = undefined;
  private static keepAliveTimer: number = -1;
  private static cachedWantAgent: WantAgent | undefined = undefined;
  private static provider: string = '';
  private static uploadDone: boolean = false;

  // 在 EntryAbility.onCreate() 中调用
  static async init(context: common.UIAbilityContext): Promise<void> {
    StarMapLiveViewController.cachedWantAgent = await StarMapLiveViewController.buildWantAgent(context);
    console.info('[LiveView] WantAgent 已缓存');
  }

  private static async buildWantAgent(context: common.UIAbilityContext): Promise<WantAgent> {
    const info: wantAgent.WantAgentInfo = {
      wants: [{
        bundleName: context.abilityInfo.bundleName,
        abilityName: 'EntryAbility'
      } as Want],
      actionType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    return await wantAgent.getWantAgent(info);
  }
}

为什么提前缓存? wantAgent.getWantAgent 是异步操作,如果在上传开始时才构建会导致实况窗创建延迟。在 onCreate 中提前构建,上传时直接使用。

EntryAbility.onCreate 中调用:

typescript 复制代码
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  // ... 其他初始化
  void StarMapLiveViewController.init(this.context);
}

3.2 创建实况窗:startUpload

上传开始时创建实况窗,展示初始进度 0/N:

typescript 复制代码
static async startUpload(total: number, provider: string): Promise<void> {
  StarMapLiveViewController.uploadDone = false;
  StarMapLiveViewController.provider = provider;

  StarMapLiveViewController.liveView = {
    id: 1001,
    event: 'DELIVERY',
    sequence: 1,
    isMute: false,
    liveViewData: {
      primary: {
        title: '云星图上传中',
        content: [{ text: `已完成 0/${total}` }],
        keepTime: 15,                                           // 15 秒后自动消失
        clickAction: StarMapLiveViewController.cachedWantAgent!, // 点击回跳应用
        extensionData: {
          text: provider,                                        // 显示图床厂商名
          type: liveViewManager.ExtensionType.EXTENSION_TYPE_COMMON_TEXT
        },
        layoutData: {
          layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
          progress: 0,                                           // 进度 0%
          color: '#FF3880F4',                                    // 进度条颜色(蓝)
          backgroundColor: '#33FFFFFF',                          // 背景色
          nodeIcons: ['start.png', 'ok.png']                     // 起止图标
        }
      },
      capsule: {
        type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
        status: 1,
        icon: 'logo.png',                                        // 应用图标
        title: '云星图',
        content: `0/${total}`,                                   // 胶囊显示 0/N
        backgroundColor: '#FF3497FF'
      }
    }
  };

  try {
    await liveViewManager.startLiveView(StarMapLiveViewController.liveView);
    StarMapLiveViewController.startKeepAlive();
    console.info(`[LiveView] 开始上传 ${total} 张`);
  } catch (e) {
    const err = e as BusinessError;
    if (err.code === 1003500006) {
      // 实况窗已存在(上次未被正确关闭),直接更新
      StarMapLiveViewController.liveView.sequence =
        (StarMapLiveViewController.liveView.sequence ?? 1) + 1;
      await liveViewManager.updateLiveView(StarMapLiveViewController.liveView);
      StarMapLiveViewController.startKeepAlive();
      return;
    }
    console.error(`[LiveView] startLiveView error: ${err.code} ${err.message}`);
    StarMapLiveViewController.liveView = undefined;
  }
}

创建流程:
#mermaid-svg-A3h2gK3ORU4DP4v9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-A3h2gK3ORU4DP4v9 .error-icon{fill:#552222;}#mermaid-svg-A3h2gK3ORU4DP4v9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-A3h2gK3ORU4DP4v9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .marker.cross{stroke:#333333;}#mermaid-svg-A3h2gK3ORU4DP4v9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-A3h2gK3ORU4DP4v9 p{margin:0;}#mermaid-svg-A3h2gK3ORU4DP4v9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .cluster-label text{fill:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .cluster-label span{color:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .cluster-label span p{background-color:transparent;}#mermaid-svg-A3h2gK3ORU4DP4v9 .label text,#mermaid-svg-A3h2gK3ORU4DP4v9 span{fill:#333;color:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .node rect,#mermaid-svg-A3h2gK3ORU4DP4v9 .node circle,#mermaid-svg-A3h2gK3ORU4DP4v9 .node ellipse,#mermaid-svg-A3h2gK3ORU4DP4v9 .node polygon,#mermaid-svg-A3h2gK3ORU4DP4v9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .rough-node .label text,#mermaid-svg-A3h2gK3ORU4DP4v9 .node .label text,#mermaid-svg-A3h2gK3ORU4DP4v9 .image-shape .label,#mermaid-svg-A3h2gK3ORU4DP4v9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-A3h2gK3ORU4DP4v9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .rough-node .label,#mermaid-svg-A3h2gK3ORU4DP4v9 .node .label,#mermaid-svg-A3h2gK3ORU4DP4v9 .image-shape .label,#mermaid-svg-A3h2gK3ORU4DP4v9 .icon-shape .label{text-align:center;}#mermaid-svg-A3h2gK3ORU4DP4v9 .node.clickable{cursor:pointer;}#mermaid-svg-A3h2gK3ORU4DP4v9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .arrowheadPath{fill:#333333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A3h2gK3ORU4DP4v9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-A3h2gK3ORU4DP4v9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A3h2gK3ORU4DP4v9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-A3h2gK3ORU4DP4v9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .cluster text{fill:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 .cluster span{color:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-A3h2gK3ORU4DP4v9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-A3h2gK3ORU4DP4v9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-A3h2gK3ORU4DP4v9 .icon-shape,#mermaid-svg-A3h2gK3ORU4DP4v9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A3h2gK3ORU4DP4v9 .icon-shape p,#mermaid-svg-A3h2gK3ORU4DP4v9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-A3h2gK3ORU4DP4v9 .icon-shape .label rect,#mermaid-svg-A3h2gK3ORU4DP4v9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A3h2gK3ORU4DP4v9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-A3h2gK3ORU4DP4v9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-A3h2gK3ORU4DP4v9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否: code=1003500006
否: 其他错误
startUpload(total, provider)
构建 LiveView 对象

progress=0, keepTime=15
liveViewManager.startLiveView(lv)
成功?
startKeepAlive()

启动 10s 续期定时器
实况窗已存在

递增 sequence + updateLiveView
liveView = undefined

放弃创建
实况窗展示中

进度 0/N

关键设计点:

  • id: 1001:实况窗唯一标识,同一 id 的实况窗全局只有一个。更新和关闭都基于这个 id。
  • event: 'DELIVERY' :事件类型,系统据此选择展示模板。常用值有 DELIVERY(配送/进行中场景,适合上传进度展示)、TAXI(出行场景)、FLIGHT(航班场景)等。
  • sequence: 1 :序列号,每次 updateLiveView 必须比上次大,否则系统忽略更新。
  • keepTime: 15:实况窗存活 15 秒,到期自动消失。通过 keepAlive 续期机制保持长期显示。
  • 错误码 1003500006:实况窗已存在(上次未正确关闭),此时不能再次 startLiveView,改为 updateLiveView。
  • nodeIcons: ['start.png', 'ok.png'] :进度条两端的图标,开始和完成时分别展示。图片需放在 resources/rawfile/ 目录下。

3.3 更新进度:updateProgress

每上传完一张图片调用一次,更新进度条、文案和胶囊:

typescript 复制代码
static async updateProgress(completed: number, total: number): Promise<void> {
  if (StarMapLiveViewController.liveView === undefined) return;

  const done: boolean = completed >= total && total > 0;
  const progress: number = total > 0 ? Math.round((completed / total) * 100) : 0;

  // 1. 递增序列号(必须比上次大)
  StarMapLiveViewController.liveView.sequence =
    (StarMapLiveViewController.liveView.sequence ?? 1) + 1;

  // 2. 更新主体区域
  StarMapLiveViewController.liveView.liveViewData.primary.title =
    done ? '上传完成' : '云星图上传中';
  StarMapLiveViewController.liveView.liveViewData.primary.content =
    [{ text: done ? `共 ${total} 张` : `已完成 ${completed}/${total}` }];

  // 3. 更新进度条
  const ld = StarMapLiveViewController.liveView.liveViewData.primary.layoutData
    as liveViewManager.ProgressLayout;
  if (ld !== undefined) {
    ld.progress = progress;
  }

  // 4. 更新胶囊
  if (StarMapLiveViewController.liveView.liveViewData.capsule !== undefined) {
    StarMapLiveViewController.liveView.liveViewData.capsule.content =
      done ? '上传完成' : `${completed}/${total}`;
  }

  // 5. 提交更新
  try {
    await liveViewManager.updateLiveView(StarMapLiveViewController.liveView);
  } catch (e) {
    const err = e as BusinessError;
    console.error(`[LiveView] updateLiveView error: ${err.code} ${err.message}`);
  }

  if (done) {
    StarMapLiveViewController.uploadDone = true;
  }
}

3.4 展示失败:showFailed

上传出现失败时,切换为红色失败样式:

typescript 复制代码
static async showFailed(successCount: number, failedCount: number, total: number): Promise<void> {
  if (StarMapLiveViewController.liveView === undefined) return;

  const remaining: number = total - successCount - failedCount;
  StarMapLiveViewController.liveView.sequence =
    (StarMapLiveViewController.liveView.sequence ?? 1) + 1;

  // 切换失败样式
  StarMapLiveViewController.liveView.liveViewData.primary.title = '上传失败';
  StarMapLiveViewController.liveView.liveViewData.primary.content =
    [{ text: `成功 ${successCount} 张,剩余 ${remaining} 张` }];

  const ld = StarMapLiveViewController.liveView.liveViewData.primary.layoutData
    as liveViewManager.ProgressLayout;
  if (ld !== undefined) {
    ld.progress = 100;
    ld.color = '#FFFF4444';           // 红色进度条
    ld.nodeIcons = ['start.png', 'fail.png'];  // 失败图标
  }

  if (StarMapLiveViewController.liveView.liveViewData.capsule !== undefined) {
    StarMapLiveViewController.liveView.liveViewData.capsule.content = '上传失败';
  }

  try {
    await liveViewManager.updateLiveView(StarMapLiveViewController.liveView);
  } catch (e) {
    const err = e as BusinessError;
    console.error(`[LiveView] showFailed error: ${err.code} ${err.message}`);
  }
  StarMapLiveViewController.uploadDone = true;
}

三种状态的样式对比:
#mermaid-svg-t6XoDNcumGSk1gdV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t6XoDNcumGSk1gdV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t6XoDNcumGSk1gdV .error-icon{fill:#552222;}#mermaid-svg-t6XoDNcumGSk1gdV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t6XoDNcumGSk1gdV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t6XoDNcumGSk1gdV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t6XoDNcumGSk1gdV .marker.cross{stroke:#333333;}#mermaid-svg-t6XoDNcumGSk1gdV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t6XoDNcumGSk1gdV p{margin:0;}#mermaid-svg-t6XoDNcumGSk1gdV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-t6XoDNcumGSk1gdV .cluster-label text{fill:#333;}#mermaid-svg-t6XoDNcumGSk1gdV .cluster-label span{color:#333;}#mermaid-svg-t6XoDNcumGSk1gdV .cluster-label span p{background-color:transparent;}#mermaid-svg-t6XoDNcumGSk1gdV .label text,#mermaid-svg-t6XoDNcumGSk1gdV span{fill:#333;color:#333;}#mermaid-svg-t6XoDNcumGSk1gdV .node rect,#mermaid-svg-t6XoDNcumGSk1gdV .node circle,#mermaid-svg-t6XoDNcumGSk1gdV .node ellipse,#mermaid-svg-t6XoDNcumGSk1gdV .node polygon,#mermaid-svg-t6XoDNcumGSk1gdV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t6XoDNcumGSk1gdV .rough-node .label text,#mermaid-svg-t6XoDNcumGSk1gdV .node .label text,#mermaid-svg-t6XoDNcumGSk1gdV .image-shape .label,#mermaid-svg-t6XoDNcumGSk1gdV .icon-shape .label{text-anchor:middle;}#mermaid-svg-t6XoDNcumGSk1gdV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-t6XoDNcumGSk1gdV .rough-node .label,#mermaid-svg-t6XoDNcumGSk1gdV .node .label,#mermaid-svg-t6XoDNcumGSk1gdV .image-shape .label,#mermaid-svg-t6XoDNcumGSk1gdV .icon-shape .label{text-align:center;}#mermaid-svg-t6XoDNcumGSk1gdV .node.clickable{cursor:pointer;}#mermaid-svg-t6XoDNcumGSk1gdV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-t6XoDNcumGSk1gdV .arrowheadPath{fill:#333333;}#mermaid-svg-t6XoDNcumGSk1gdV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-t6XoDNcumGSk1gdV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-t6XoDNcumGSk1gdV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t6XoDNcumGSk1gdV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t6XoDNcumGSk1gdV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t6XoDNcumGSk1gdV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-t6XoDNcumGSk1gdV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-t6XoDNcumGSk1gdV .cluster text{fill:#333;}#mermaid-svg-t6XoDNcumGSk1gdV .cluster span{color:#333;}#mermaid-svg-t6XoDNcumGSk1gdV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-t6XoDNcumGSk1gdV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t6XoDNcumGSk1gdV rect.text{fill:none;stroke-width:0;}#mermaid-svg-t6XoDNcumGSk1gdV .icon-shape,#mermaid-svg-t6XoDNcumGSk1gdV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t6XoDNcumGSk1gdV .icon-shape p,#mermaid-svg-t6XoDNcumGSk1gdV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-t6XoDNcumGSk1gdV .icon-shape .label rect,#mermaid-svg-t6XoDNcumGSk1gdV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t6XoDNcumGSk1gdV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-t6XoDNcumGSk1gdV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-t6XoDNcumGSk1gdV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上传失败
title: 上传失败
content: 成功 7 张,剩余 3 张
progress: 100%
color: #FF4444 红色
icons: start.png + fail.png
capsule: 上传失败
上传完成
title: 上传完成
content: 共 10 张
progress: 100%
color: #3880F4 蓝色
icons: start.png + ok.png
capsule: 上传完成
上传中
title: 云星图上传中
content: 已完成 3/10
progress: 30%
color: #3880F4 蓝色
icons: start.png + ok.png
capsule: 3/10

效果展示:

3.5 keepAlive 续期机制

实况窗的 keepTime 默认只有 15 秒,到期后自动消失。对于上传这种耗时操作,需要定期续期:

typescript 复制代码
// 每 10 秒续期一次,防止 keepTime 到期自动消失
private static startKeepAlive(): void {
  StarMapLiveViewController.stopKeepAlive();
  StarMapLiveViewController.keepAliveTimer = setInterval(() => {
    if (StarMapLiveViewController.liveView !== undefined) {
      // 续期 = 递增 sequence + 调用 updateLiveView
      StarMapLiveViewController.liveView.sequence =
        (StarMapLiveViewController.liveView.sequence ?? 1) + 1;
      liveViewManager.updateLiveView(StarMapLiveViewController.liveView).catch(() => {});
    }
  }, 10000);
}

private static stopKeepAlive(): void {
  if (StarMapLiveViewController.keepAliveTimer !== -1) {
    clearInterval(StarMapLiveViewController.keepAliveTimer);
    StarMapLiveViewController.keepAliveTimer = -1;
  }
}

为什么是 10 秒? keepTime 设为 15 秒,续期间隔 10 秒,留 5 秒余量,确保在到期前完成续期。

续期机制的工作原理:
系统 liveViewManager keepAlive 定时器 应用 系统 liveViewManager keepAlive 定时器 应用 #mermaid-svg-LcwcScpxMssWiRaw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LcwcScpxMssWiRaw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LcwcScpxMssWiRaw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LcwcScpxMssWiRaw .error-icon{fill:#552222;}#mermaid-svg-LcwcScpxMssWiRaw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LcwcScpxMssWiRaw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LcwcScpxMssWiRaw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LcwcScpxMssWiRaw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LcwcScpxMssWiRaw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LcwcScpxMssWiRaw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LcwcScpxMssWiRaw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LcwcScpxMssWiRaw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LcwcScpxMssWiRaw .marker.cross{stroke:#333333;}#mermaid-svg-LcwcScpxMssWiRaw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LcwcScpxMssWiRaw p{margin:0;}#mermaid-svg-LcwcScpxMssWiRaw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LcwcScpxMssWiRaw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-LcwcScpxMssWiRaw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LcwcScpxMssWiRaw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-LcwcScpxMssWiRaw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-LcwcScpxMssWiRaw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-LcwcScpxMssWiRaw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-LcwcScpxMssWiRaw .sequenceNumber{fill:white;}#mermaid-svg-LcwcScpxMssWiRaw #sequencenumber{fill:#333;}#mermaid-svg-LcwcScpxMssWiRaw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-LcwcScpxMssWiRaw .messageText{fill:#333;stroke:none;}#mermaid-svg-LcwcScpxMssWiRaw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LcwcScpxMssWiRaw .labelText,#mermaid-svg-LcwcScpxMssWiRaw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-LcwcScpxMssWiRaw .loopText,#mermaid-svg-LcwcScpxMssWiRaw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-LcwcScpxMssWiRaw .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LcwcScpxMssWiRaw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-LcwcScpxMssWiRaw .noteText,#mermaid-svg-LcwcScpxMssWiRaw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-LcwcScpxMssWiRaw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LcwcScpxMssWiRaw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LcwcScpxMssWiRaw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LcwcScpxMssWiRaw .actorPopupMenu{position:absolute;}#mermaid-svg-LcwcScpxMssWiRaw .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-LcwcScpxMssWiRaw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LcwcScpxMssWiRaw .actor-man circle,#mermaid-svg-LcwcScpxMssWiRaw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-LcwcScpxMssWiRaw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} --- 每 10 秒续期 --- 保持显示 loop 每 10 秒 --- 上传完成 --- startLiveView(keepTime=15s) 实况窗创建,15s 后将消失 updateLiveView(sequence++) 重置 keepTime 计时器 stopKeepAlive() stopLiveView(keepTime=0) 立即关闭实况窗

3.6 关闭实况窗:stop

上传完成或应用退出时关闭实况窗:

typescript 复制代码
static async stop(): Promise<void> {
  StarMapLiveViewController.stopKeepAlive();  // 先停止续期定时器

  if (StarMapLiveViewController.liveView === undefined) return;

  const lv = StarMapLiveViewController.liveView;
  lv.sequence = (lv.sequence ?? 1) + 1;
  lv.liveViewData.primary.title = '上传完成';
  lv.liveViewData.primary.content = [{ text: '所有图片已上传' }];
  lv.liveViewData.primary.keepTime = 0;       // 设为 0 立即关闭
  const ld = lv.liveViewData.primary.layoutData as liveViewManager.ProgressLayout;
  if (ld !== undefined) {
    ld.progress = 100;
  }
  if (lv.liveViewData.capsule !== undefined) {
    lv.liveViewData.capsule.content = '上传完成';
  }

  try {
    await liveViewManager.stopLiveView(lv);
    console.info('[LiveView] 实况窗已关闭');
  } catch (e) {
    const err = e as BusinessError;
    console.error(`[LiveView] stop error: ${err.code} ${err.message}`);
  }
  StarMapLiveViewController.liveView = undefined;
}

关键点: keepTime = 0 是关闭实况窗的信号,系统收到后会立即移除实况窗。

四、与上传流程集成

4.1 上传流程中的调用

NormalUpLoad.doBatchUpload 中,实况窗与上传循环、后台任务紧密配合:

typescript 复制代码
private async doBatchUpload(config: CloudConfig) {
  this.isAllUploading = true;
  const idleItems = this.uploadList.filter(item => item.status === 'idle' || item.status === 'failed');
  const total: number = idleItems.length;
  let completed: number = 0;
  let failedCount: number = 0;
  let hasFailed: boolean = false;
  const providerName: string = getProviderMeta(config.type)?.name ?? config.type;

  // 1. 创建实况窗 + 启动后台长时任务
  await StarMapLiveViewController.startUpload(total, providerName);
  const wantAgent: WantAgent | undefined = StarMapLiveViewController.getWantAgent();
  if (wantAgent !== undefined) {
    backgroundTaskManager.startBackgroundRunning(this.context,
      ['dataTransfer'], wantAgent).catch(() => {});
  }

  // 2. 逐张上传,每完成一张更新实况窗进度
  for (const item of idleItems) {
    this.updateItemStatus(item.id, 'uploading', 0);
    await this.uploadSingleItem(item, config);
    completed++;
    if (item.status === 'failed') {
      failedCount++;
      hasFailed = true;
      break;
    }
    void StarMapLiveViewController.updateProgress(completed, total);
  }

  // 3. 停止后台长时任务
  this.isAllUploading = false;
  void backgroundTaskManager.stopBackgroundRunning(this.context).catch(() => {});

  // 4. 根据结果更新实况窗最终状态
  if (hasFailed) {
    const successCount = completed - failedCount;
    void StarMapLiveViewController.showFailed(successCount, failedCount, total);
  }
  // 全部成功时,实况窗在 onForeground 中由 stop() 关闭
  // ... 省略 UI 状态更新与上传记录刷新等非实况窗相关代码
}

上传流程与实况窗的完整交互时序:
云存储 backgroundTaskManager LiveViewController NormalUpLoad 云存储 backgroundTaskManager LiveViewController NormalUpLoad #mermaid-svg-AHibFzFR4S2kwLgJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AHibFzFR4S2kwLgJ .error-icon{fill:#552222;}#mermaid-svg-AHibFzFR4S2kwLgJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AHibFzFR4S2kwLgJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AHibFzFR4S2kwLgJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AHibFzFR4S2kwLgJ .marker.cross{stroke:#333333;}#mermaid-svg-AHibFzFR4S2kwLgJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AHibFzFR4S2kwLgJ p{margin:0;}#mermaid-svg-AHibFzFR4S2kwLgJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AHibFzFR4S2kwLgJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-AHibFzFR4S2kwLgJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-AHibFzFR4S2kwLgJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-AHibFzFR4S2kwLgJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-AHibFzFR4S2kwLgJ .sequenceNumber{fill:white;}#mermaid-svg-AHibFzFR4S2kwLgJ #sequencenumber{fill:#333;}#mermaid-svg-AHibFzFR4S2kwLgJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-AHibFzFR4S2kwLgJ .messageText{fill:#333;stroke:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AHibFzFR4S2kwLgJ .labelText,#mermaid-svg-AHibFzFR4S2kwLgJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .loopText,#mermaid-svg-AHibFzFR4S2kwLgJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-AHibFzFR4S2kwLgJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-AHibFzFR4S2kwLgJ .noteText,#mermaid-svg-AHibFzFR4S2kwLgJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-AHibFzFR4S2kwLgJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AHibFzFR4S2kwLgJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AHibFzFR4S2kwLgJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AHibFzFR4S2kwLgJ .actorPopupMenu{position:absolute;}#mermaid-svg-AHibFzFR4S2kwLgJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-AHibFzFR4S2kwLgJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AHibFzFR4S2kwLgJ .actor-man circle,#mermaid-svg-AHibFzFR4S2kwLgJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-AHibFzFR4S2kwLgJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后台保活,防止进程被杀 loop 逐张上传 onForeground 时 isUploadDone=true ->> stop() alt 全部成功 有失败 startUpload(total, providerName) 创建实况窗 progress=0 startKeepAlive() startBackgroundRunning(dataTransfer) uploadObject(item) 上传成功 updateItemStatus(success) updateProgress(completed, total) sequence++ + 更新进度/文案/胶囊 stopBackgroundRunning() 刷新首页图片列表 showFailed(successCount, failedCount, total) 切换红色失败样式

4.2 生命周期管理

实况窗需要在多个生命周期节点进行管理:

typescript 复制代码
// EntryAbility.ets

async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  // 应用启动时缓存 WantAgent(不启动实况窗)
  void StarMapLiveViewController.init(this.context);
}

onForeground(): void {
  // 应用回到前台时,如果上传已完成则关闭实况窗
  if (StarMapLiveViewController.isUploadDone()) {
    void StarMapLiveViewController.stop();
  }
}

onDestroy(): void {
  // 应用销毁时关闭实况窗
  void StarMapLiveViewController.stop();
}

生命周期管理流程:
#mermaid-svg-f2E4iEFkMDIUORcX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-f2E4iEFkMDIUORcX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-f2E4iEFkMDIUORcX .error-icon{fill:#552222;}#mermaid-svg-f2E4iEFkMDIUORcX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-f2E4iEFkMDIUORcX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-f2E4iEFkMDIUORcX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-f2E4iEFkMDIUORcX .marker.cross{stroke:#333333;}#mermaid-svg-f2E4iEFkMDIUORcX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-f2E4iEFkMDIUORcX p{margin:0;}#mermaid-svg-f2E4iEFkMDIUORcX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-f2E4iEFkMDIUORcX .cluster-label text{fill:#333;}#mermaid-svg-f2E4iEFkMDIUORcX .cluster-label span{color:#333;}#mermaid-svg-f2E4iEFkMDIUORcX .cluster-label span p{background-color:transparent;}#mermaid-svg-f2E4iEFkMDIUORcX .label text,#mermaid-svg-f2E4iEFkMDIUORcX span{fill:#333;color:#333;}#mermaid-svg-f2E4iEFkMDIUORcX .node rect,#mermaid-svg-f2E4iEFkMDIUORcX .node circle,#mermaid-svg-f2E4iEFkMDIUORcX .node ellipse,#mermaid-svg-f2E4iEFkMDIUORcX .node polygon,#mermaid-svg-f2E4iEFkMDIUORcX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-f2E4iEFkMDIUORcX .rough-node .label text,#mermaid-svg-f2E4iEFkMDIUORcX .node .label text,#mermaid-svg-f2E4iEFkMDIUORcX .image-shape .label,#mermaid-svg-f2E4iEFkMDIUORcX .icon-shape .label{text-anchor:middle;}#mermaid-svg-f2E4iEFkMDIUORcX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-f2E4iEFkMDIUORcX .rough-node .label,#mermaid-svg-f2E4iEFkMDIUORcX .node .label,#mermaid-svg-f2E4iEFkMDIUORcX .image-shape .label,#mermaid-svg-f2E4iEFkMDIUORcX .icon-shape .label{text-align:center;}#mermaid-svg-f2E4iEFkMDIUORcX .node.clickable{cursor:pointer;}#mermaid-svg-f2E4iEFkMDIUORcX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-f2E4iEFkMDIUORcX .arrowheadPath{fill:#333333;}#mermaid-svg-f2E4iEFkMDIUORcX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-f2E4iEFkMDIUORcX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-f2E4iEFkMDIUORcX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-f2E4iEFkMDIUORcX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-f2E4iEFkMDIUORcX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-f2E4iEFkMDIUORcX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-f2E4iEFkMDIUORcX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-f2E4iEFkMDIUORcX .cluster text{fill:#333;}#mermaid-svg-f2E4iEFkMDIUORcX .cluster span{color:#333;}#mermaid-svg-f2E4iEFkMDIUORcX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-f2E4iEFkMDIUORcX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-f2E4iEFkMDIUORcX rect.text{fill:none;stroke-width:0;}#mermaid-svg-f2E4iEFkMDIUORcX .icon-shape,#mermaid-svg-f2E4iEFkMDIUORcX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-f2E4iEFkMDIUORcX .icon-shape p,#mermaid-svg-f2E4iEFkMDIUORcX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-f2E4iEFkMDIUORcX .icon-shape .label rect,#mermaid-svg-f2E4iEFkMDIUORcX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-f2E4iEFkMDIUORcX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-f2E4iEFkMDIUORcX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-f2E4iEFkMDIUORcX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 回到前台
上传过程
应用启动
全部完成
有失败


应用销毁
onDestroy
stop()

关闭实况窗

停止 keepAlive
onCreate
LiveViewController.init

缓存 WantAgent
应用就绪

(无实况窗)
doBatchUpload
startUpload

创建实况窗 + keepAlive
逐张上传
updateProgress

更新进度
uploadDone = true
showFailed

红色失败样式
onForeground
isUploadDone()?
stop()

关闭实况窗
保持实况窗

继续展示进度

为什么在 onForeground 中关闭? 上传完成时应用可能在后台,用户看到实况窗显示"上传完成"后回到应用。此时实况窗已无意义,在 onForeground 中检测到 uploadDone = true 就关闭它,提供干净的体验。

五、后台长时任务配合

实况窗展示上传进度时,上传任务本身需要在后台持续运行。backgroundTaskManager 提供了长时任务保活能力:

typescript 复制代码
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { WantAgent } from '@kit.AbilityKit';

// 开始上传时启动后台长时任务
const wantAgent: WantAgent | undefined = StarMapLiveViewController.getWantAgent();
if (wantAgent !== undefined) {
  backgroundTaskManager.startBackgroundRunning(this.context,
    ['dataTransfer'], wantAgent).catch(() => {});
}

// 上传完成后停止
void backgroundTaskManager.stopBackgroundRunning(this.context).catch(() => {});

实况窗与后台任务的协作关系:
#mermaid-svg-E6j3kzsH4uM2LLB8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E6j3kzsH4uM2LLB8 .error-icon{fill:#552222;}#mermaid-svg-E6j3kzsH4uM2LLB8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E6j3kzsH4uM2LLB8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .marker.cross{stroke:#333333;}#mermaid-svg-E6j3kzsH4uM2LLB8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E6j3kzsH4uM2LLB8 p{margin:0;}#mermaid-svg-E6j3kzsH4uM2LLB8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .cluster-label text{fill:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .cluster-label span{color:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .cluster-label span p{background-color:transparent;}#mermaid-svg-E6j3kzsH4uM2LLB8 .label text,#mermaid-svg-E6j3kzsH4uM2LLB8 span{fill:#333;color:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .node rect,#mermaid-svg-E6j3kzsH4uM2LLB8 .node circle,#mermaid-svg-E6j3kzsH4uM2LLB8 .node ellipse,#mermaid-svg-E6j3kzsH4uM2LLB8 .node polygon,#mermaid-svg-E6j3kzsH4uM2LLB8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .rough-node .label text,#mermaid-svg-E6j3kzsH4uM2LLB8 .node .label text,#mermaid-svg-E6j3kzsH4uM2LLB8 .image-shape .label,#mermaid-svg-E6j3kzsH4uM2LLB8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-E6j3kzsH4uM2LLB8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .rough-node .label,#mermaid-svg-E6j3kzsH4uM2LLB8 .node .label,#mermaid-svg-E6j3kzsH4uM2LLB8 .image-shape .label,#mermaid-svg-E6j3kzsH4uM2LLB8 .icon-shape .label{text-align:center;}#mermaid-svg-E6j3kzsH4uM2LLB8 .node.clickable{cursor:pointer;}#mermaid-svg-E6j3kzsH4uM2LLB8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .arrowheadPath{fill:#333333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E6j3kzsH4uM2LLB8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-E6j3kzsH4uM2LLB8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E6j3kzsH4uM2LLB8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-E6j3kzsH4uM2LLB8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .cluster text{fill:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 .cluster span{color:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-E6j3kzsH4uM2LLB8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-E6j3kzsH4uM2LLB8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-E6j3kzsH4uM2LLB8 .icon-shape,#mermaid-svg-E6j3kzsH4uM2LLB8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E6j3kzsH4uM2LLB8 .icon-shape p,#mermaid-svg-E6j3kzsH4uM2LLB8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-E6j3kzsH4uM2LLB8 .icon-shape .label rect,#mermaid-svg-E6j3kzsH4uM2LLB8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E6j3kzsH4uM2LLB8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-E6j3kzsH4uM2LLB8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-E6j3kzsH4uM2LLB8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上传结束
stopBackgroundRunning

释放后台保活
stop / showFailed

关闭或切换失败样式
stopKeepAlive

停止续期定时器
上传进行中
后台持续上传
updateProgress

更新实况窗
keepAlive 定时器

每 10s 续期实况窗
上传开始
startBackgroundRunning

dataTransfer + WantAgent
系统保活进程

允许后台网络传输
startLiveView

实况窗展示进度
startKeepAlive

10s 续期

两者的分工:

后台长时任务 实况窗
作用 防止应用进程被系统杀死 向用户展示任务进度
核心 API backgroundTaskManager liveViewManager
启动时机 上传开始 上传开始
停止时机 上传结束 上传结束 + 应用回前台
需要权限 KEEP_BACKGROUND_RUNNING 无(需 AGC 权益)
需要配置 backgroundModes

六、完整 API 速查

6.1 StarMapLiveViewController 方法

方法 调用时机 作用
init(context) EntryAbility.onCreate 缓存 WantAgent
startUpload(total, provider) 上传开始 创建实况窗 + 启动续期
updateProgress(completed, total) 每完成一张 更新进度/文案/胶囊
showFailed(success, fail, total) 上传有失败 切换红色失败样式
stop() 上传完成/应用销毁 关闭实况窗 + 停止续期
isUploadDone() onForeground 查询上传是否完成
getWantAgent() 启动后台任务时 获取缓存的 WantAgent

6.2 liveViewManager API

API 作用 注意事项
startLiveView(lv) 创建实况窗 同 id 已存在会报 1003500006
updateLiveView(lv) 更新实况窗 sequence 必须比上次大
stopLiveView(lv) 关闭实况窗 keepTime 设为 0

6.3 LiveView 关键字段

字段 说明 必填
id 实况窗唯一标识
sequence 序列号,每次更新必须递增
liveViewData.primary.title 主体标题
liveViewData.primary.content 主体内容文案数组
liveViewData.primary.keepTime 存活秒数,0 = 立即关闭
liveViewData.primary.clickAction 点击回跳的 WantAgent
liveViewData.primary.layoutData 进度条布局(ProgressLayout)
liveViewData.capsule 胶囊区域

七、常见问题与最佳实践

7.1 常见问题

问题 原因 解决
startLiveView 报 67108900 AGC 未开通 Live View Kit 权益 按 2.1 节步骤在 AGC 开通权益或申请白名单
实况窗更新不生效 sequence 未递增或比上次小 每次更新前 sequence = sequence + 1
实况窗 15 秒后消失 未启动 keepAlive 续期 startUpload 后调用 startKeepAlive
startLiveView 报 1003500006 上次实况窗未正确关闭 catch 中改为 updateLiveView
后台上传被系统杀进程 未启动后台长时任务 配合 backgroundTaskManager.startBackgroundRunning
点击实况窗无反应 未设置 clickAction 或 WantAgent 无效 onCreate 中提前 init 缓存 WantAgent
应用销毁后实况窗残留 未在 onDestroy 中关闭 onDestroy 调用 stop()

7.2 最佳实践

  1. 提前缓存 WantAgentwantAgent.getWantAgent 是异步操作,在 onCreate 中提前构建,避免上传时延迟。

  2. sequence 严格递增 :系统通过 sequence 判断是否为有效更新。每次 updateLiveViewstopLiveView 前都必须递增。

  3. keepAlive 间隔小于 keepTimekeepTime = 15s,续期间隔 10s,留 5 秒余量确保不中断。

  4. 错误码 1003500006 容错startLiveView 失败时检查是否为"已存在"错误,改为 updateLiveView 而非直接放弃。

  5. onForeground 检测关闭:用户回到应用时如果上传已完成,及时关闭实况窗,避免残留。

  6. 后台任务配对startBackgroundRunningstopBackgroundRunning 必须严格配对,否则系统会一直保活进程。

  7. 失败样式区分:用红色进度条 + 失败图标让用户一眼看出失败,比纯文字更直观。

八、总结

实况窗是展示进行中任务进度的最佳方式,本教程以图片上传为例,讲解了完整实现:

  • 初始化onCreate 中缓存 WantAgent,避免异步延迟
  • 创建startUpload 构建 LiveView(进度条布局 + 胶囊),处理"已存在"错误码
  • 更新updateProgress 递增 sequence + 更新进度/文案/胶囊
  • 续期keepAlive 每 10 秒 updateLiveView 防止 keepTime 到期消失
  • 失败showFailed 切换红色进度条 + 失败图标
  • 关闭stopkeepTime=0 + stopLiveView + 停止续期定时器
  • 后台配合backgroundTaskManager 长时任务保活,backgroundModes: ["dataTransfer"] 声明后台模式

开发建议:

  1. 实况窗和后台长时任务成对使用,前者展示进度,后者保活进程
  2. sequence 每次更新必须递增,否则系统忽略
  3. keepAlive 间隔必须小于 keepTime,留足余量
  4. 在 onForeground 和 onDestroy 中都处理关闭,避免残留
  5. startLiveView 的 1003500006 错误码要 catch 并改为 updateLiveView