HarmonyOS 实况窗教程:上传进度实时展示
一、概述
实况窗(Live View)是 HarmonyOS 提供的系统级实时信息展示能力,可将应用的进行中任务以卡片形式展示在实况窗、通知中心和锁屏上。与普通通知不同,实况窗支持实时更新进度 、点击回跳应用。
本教程以「云星图」项目的图片上传场景为例,讲解:
- 实况窗的创建与进度更新 -- 进度条布局 + 胶囊状态 + 序列号递增
- keepAlive 续期机制 -- 防止 keepTime 到期导致实况窗消失
- 与后台长时任务联动 -- 后台上传时不被系统杀进程
- WantAgent 点击回跳 -- 用户点击实况窗回到应用
- 生命周期管理 -- 初始化/更新/失败/关闭的完整闭环
整体架构如下:
#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开发者
开通步骤:
- 登录 AppGallery Connect 后台
- 进入「我的项目」-> 选择目标项目 -> 「HarmonyOS 应用」
- 在「API 管理」或「增长」->「实况窗」中找到 Live View Kit
- 点击「开通」并提交场景说明文档(描述使用实况窗的业务场景)
- 等待审核(约 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 最佳实践
-
提前缓存 WantAgent :
wantAgent.getWantAgent是异步操作,在onCreate中提前构建,避免上传时延迟。 -
sequence 严格递增 :系统通过 sequence 判断是否为有效更新。每次
updateLiveView和stopLiveView前都必须递增。 -
keepAlive 间隔小于 keepTime :
keepTime = 15s,续期间隔10s,留 5 秒余量确保不中断。 -
错误码 1003500006 容错 :
startLiveView失败时检查是否为"已存在"错误,改为updateLiveView而非直接放弃。 -
onForeground 检测关闭:用户回到应用时如果上传已完成,及时关闭实况窗,避免残留。
-
后台任务配对 :
startBackgroundRunning和stopBackgroundRunning必须严格配对,否则系统会一直保活进程。 -
失败样式区分:用红色进度条 + 失败图标让用户一眼看出失败,比纯文字更直观。
八、总结
实况窗是展示进行中任务进度的最佳方式,本教程以图片上传为例,讲解了完整实现:
- 初始化 :
onCreate中缓存 WantAgent,避免异步延迟 - 创建 :
startUpload构建 LiveView(进度条布局 + 胶囊),处理"已存在"错误码 - 更新 :
updateProgress递增 sequence + 更新进度/文案/胶囊 - 续期 :
keepAlive每 10 秒updateLiveView防止keepTime到期消失 - 失败 :
showFailed切换红色进度条 + 失败图标 - 关闭 :
stop设keepTime=0+stopLiveView+ 停止续期定时器 - 后台配合 :
backgroundTaskManager长时任务保活,backgroundModes: ["dataTransfer"]声明后台模式
开发建议:
- 实况窗和后台长时任务成对使用,前者展示进度,后者保活进程
- sequence 每次更新必须递增,否则系统忽略
- keepAlive 间隔必须小于 keepTime,留足余量
- 在 onForeground 和 onDestroy 中都处理关闭,避免残留
- startLiveView 的 1003500006 错误码要 catch 并改为 updateLiveView