Progress(进度条)是最"哑"的组件——画一条线或一个环,填个百分比就完事。但健康菜谱助手做营养摄入环时发现,进度体系真正难的不是画,是"什么时候该动、动到哪、动错了怎么撤"。这篇复盘 Progress 体系(线性条+环形+加载态)的完整实践:确定性进度的三段式动画、不确定加载的语义区分、以及一个反向进度的事故。

进度体系

一、两种进度,两种语义:先分清再动手

进度组件第一课:确定性进度(Progress 型,有明确百分比)与不确定加载(Loading 型,转圈)是两个物种,混用是最大误区。导入菜谱文件用确定性(文件可解析,字节数即进度);登录跳转用不确定(不知道要几秒)。混用的典型事故:把网络请求做成"假进度"——前端不知道后台进度,放一个匀速爬到 90% 的条,卡在 90% 等接口回来才跳 100%。用户看到 90% 卡了三秒,以为死了。健康菜谱助手早期就犯过,改版原则一句话:不知道进度的场景绝不假装知道,老老实实转圈;知道进度就给真数字,别拿动画糊弄。

场景类型原因
菜谱文件导入确定性 Progress字节数可算
登录跳转Loading 转圈时长不可知
营养环确定性+目标值摄入/目标比例
首图加载占位骨架屏占比无意义,布局先行

二、确定性进度:三段式动画的节奏

真正的进度也要讲究节奏。文件导入实测:匀速爬升的条,用户在前 20% 觉得快、后 80% 觉得慢——因为人对"剩余时间"的感知基于速度趋势而非绝对值。改进成三段式:起步快(ease-out,前 10% 迅速走完给信心)、中段匀速、收尾减速(最后 10% 放缓,配合完成动画衔接)。实现上是把真实百分比做一次缓动映射再喂给组件,真实数据不变,感知速度变顺。配套一个纪律:进度条必须与实际进度单调对应,宁可跳变不可回退——回退的进度条比匀速的更让用户恐慌。

// 真实百分比 → 感知百分比(三段缓动映射)
function feltProgress(raw: number): number {
  if (raw < 0.1) return raw * 2                       // 起步加倍
  if (raw > 0.9) return 0.8 + (raw - 0.9) * 2         // 收尾减速
  return 0.2 + (raw - 0.1) * 0.75                     // 中段线性
}
Progress({ value: feltProgress(raw) * 100, total: 100 })

三、环形进度:营养环的复用与克制

营养摄入环用 Progress 环形模式实现,与 DataPanel 的分工:单指标完成度用 Progress 环(只表达"走了多少"),多段构成用 DataPanel(表达"构成比")。Progress 环的工程点:颜色随区间变化(0-70% 蓝、70-99% 橙、100% 绿),颜色切换要在 95% 和 100% 两个临界点做滞回(超过 97% 才变绿,掉回 95% 才变回橙),避免临界值抖动造成颜色闪烁。环上不直接叠文字,中心留白放结论句——这个规范从 DataPanel 那边继承,两个组件的阅读体验保持一致。

// 区间色+滞回防抖
ringColor(cur: number, prev: number): ResourceColor {
  if (cur >= 97 || (prev >= 97 && cur >= 95)) return C_GREEN   // 滞回带
  if (cur >= 70) return C_AMBER
  return C_BLUE
}

环形进度

四、加载态:骨架屏优先于转圈

首屏图片、列表块的加载占位,骨架屏优于转圈:骨架保住了布局结构(减少布局偏移 CLS),转圈是全局抽象的"等待"信息。骨架的实现三要点:占位尺寸必须与真实内容一致(不然加载完成瞬间布局跳动);闪烁动画用低频(1.2 秒周期,高频闪烁像故障);超过 3 秒的骨架自动降级为重试按钮(长时间的骨架等于卡死的遮羞布)。健康菜谱助手的首页首图全部骨架化后,CLS 指标从 0.18 降到 0.03,视觉稳定性的提升用户是能感受到的——虽然他们说不出原因。

五、真实事故:反向进度

上线一个版本后用户反馈"导入条会倒着走"。排查:文件导入是分块读取,每块完成后回调更新进度;当文件流重试(网络断连重连)时,分块从头开始,进度从 80% 跳回 30% 再往前爬。根因是进度源用"已处理字节数"而不是"全局偏移"——重试后已处理数清零,进度自然倒退。修复:进度计算改为"按文件全局偏移",重试时从断点偏移继续,配合 UI 端单调锁(Math.max(现值,新值)),双保险。这次事故的教训:进度条的数字来源必须是单调递增的物理量,任何"重算"都可能引入回退,UI 层的最后一把锁永远不能省。

// 单调锁:UI层最后防线
updateProgress(raw: number) {
  this.shown = Math.max(this.shown, feltProgress(raw))   // 只进不退
}

反向进度事故

六、完成态:100% 不是终点

进度到 100% 的瞬间是体验设计的黄金三秒:条要有一个明确的"完成动画"(填充色变成功色+短促缩放),随即过渡到结果内容——停留在 100% 的满条超过一秒,用户的等待焦虑会重新爬上来。实现:进度完成回调里先播 300 毫秒完成动画,再切换到结果视图,两段衔接在同一帧。失败态同样要有出口:导入失败时条变红+错误摘要+重试按钮,不能让红条孤零零挂着。完成与失败都有确定的视觉终态,进度组件才算闭环——满条不动和空转都是"没有终态"的表现。

六、批量导入:多文件的进度聚合

营养方案支持一次导入多个菜谱文件,多文件的进度聚合是单文件之外的另一层。聚合策略:总进度=已完成文件数+当前文件进度,分母是总文件数——"文件 3/10"的粗粒度加"当前文件 47%"的细粒度,两层叠加。实现的坑在分母:文件列表是异步枚举的(先扫目录再逐个读),扫描完成前总文件数未知,总进度在扫描期只能显示"枚举中"的转圈态。另一个坑在失败文件:10 个文件 1 个失败,进度是停在 90% 还是跳 100%?答案是完成动画照常播(9 个成功已是有效结果),失败文件单独列在结果页的失败区,不拖累整体进度。多文件聚合的设计原则:进度条表达"整体完成度",成败明细交给结果页,一个组件不背两种信息。

// 两层聚合:文件数粒度×文件内粒度
totalProgress(doneFiles: number, curFile: number, fileCount: number): number {
  if (fileCount === 0) return 0
  return (doneFiles + curFile) / fileCount * 100
}
// 扫描期 fileCount 未知 → 显示 Loading 态而非假进度

七、断点续传:进度与恢复的配合

大文件导入中途被杀(用户切走、系统回收),下次打开要从断点继续——进度组件与持久化的配合决定恢复体验。实现:每完成一个分块,把全局偏移写入持久化(Preferences 足够,不必上数据库);重新进入导入页时读取偏移,进度条直接定位到断点位置起步。配合单调锁,断点起步不会出现"从零闪一下再跳到断点"的视觉倒退。断点恢复的文案要诚实:"继续上次导入(已完成 65%)"——用户对"接着来"的容忍度远高于"从头再来"。这个功能上线后,大文件导入的放弃率从 31% 降到 8%,用户不是没有耐心,是不能接受白等。

// 断点:偏移持久化+恢复起步
async saveOffset(offset: number) {
  preferences.putSync('import_offset', offset)
  await preferences.flush()
}
// 恢复:读偏移,进度条直接定位
const resumeFrom = preferences.getSync('import_offset') ?? 0
this.shown = feltProgress(resumeFrom / totalSize)

八、进度与取消:随时可退的出口

长进度操作必须有取消出口,而且取消要"立即生效"——点取消后进度条还继续爬两秒才停,用户的取消信任就被消耗一次。实现要点:取消标志是共享状态,分块循环每轮开头检查,命中即跳出并清理临时文件;UI 上点取消的瞬间进度条置灰停动+文案切"正在取消…",给操作一个可视的确认;取消完成的提示与失败区分("已取消"和"导入失败"是两种不同的事实)。取消还有一个容易漏的边界:取消发生在最后一块写入的瞬间,结果可能是"实际已完成但 UI 显示已取消"——处理方式是取消检查放在事务边界,已提交的块不回滚,完成提示照常给,只是不再继续后续块。

// 取消:分块边界检查,UI即时反馈
for (let i = startChunk; i < chunks.length; i++) {
  if (this.cancelled) { this.cleanup(); this.showCancelled(); return }
  await this.importChunk(chunks[i])
  this.updateProgress(this.globalOffset())
}

九、设计溯源:进度条为什么长这样

补一段设计考古,帮助理解规范背后的逻辑。进度条的原型是工业仪表的行程指示,所以它天然带着两个隐喻:方向感(从起点到终点,不可逆)与预期感(位置对应剩余量)。今天所有交互规范其实都是这两个隐喻的延伸——单调性维护方向感,三段缓动校准预期感,终态闭环是行程的"到站"。反过来,凡是违背这两个隐喻的变体(来回摆动的"忙碌条"、无终点的循环进度)都在透支隐喻本身,用户见到一次"倒着走的进度",之后看到任何进度都会先怀疑一下。这就是为什么进度条的规范比别的组件更硬:它维护的不是组件本身,是用户对"进度"这个信息类型的信任。

十、与生态组件的配合:三种等待的分工

应用内的等待场景不止进度条一种,三者分工要清楚:骨架屏管"布局未知"(首屏、列表首刷),Loading 转圈管"时长不可知的短等待"(提交、跳转),Progress 管"时长可知的确定过程"(导入、下载)。同屏共存的原则:一个操作链路里只用一种等待表达,不叠加(下载中既有进度条又有转圈,等于告诉用户连自己都不确定在等什么)。健康菜谱助手的导入链路就是完整示范:文件选择后转圈(枚举,不可知)→ 骨架闪现(列表结构建立)→ 进度条接管(分块导入,可确定)→ 结果页。三种等待各就各位,用户在每个阶段都知道自己在等什么、还要等多久。

等待类型组件适用
布局未知骨架屏首屏/列表首刷
短等待不可知Loading提交/跳转
长等待确定Progress导入/下载

十一、验证:进度的回归清单

进度体系回归固定五项:一、单调性——任何操作序列(重试/暂停/断点恢复)下进度不回退;二、终态完备——成功、失败、取消三态各有明确视觉与文案,无悬空满条;三、节奏体感——录屏对比三段式与匀速的主观速度评价,起步收尾不突兀;四、断点恢复——中途杀进程后重进,起点为断点且文案含进度;五、无障碍——播报按 10% 步进,完成失败各一次。五项里单调性是事故专项,任何改动进度源的提交都必须跑一遍"重试不回退"用例。

十二、无障碍:进度条的读法

进度条的无障碍三层:accessibilityText 实时更新("导入中,已完成 60%")但频率要控制——每帧播报是灾难,按 10% 步进播报;完成与失败各有一次明确的播报("导入完成"/"导入失败,可重试");纯装饰性的进度(营养环已有文字结论)对读屏隐藏(focusable false),避免重复噪音。步进播报的实现是把播报逻辑放在进度计算层而非渲染层——渲染层每帧变,计算层才知道"跨过了哪个 10% 台阶"。

十二、写给评审:进度的验收小抄

给评审同学的速查口径,五分钟过完一个进度组件:打开页面看首屏有无布局跳动(骨架的活);触发一次导入看起步是否干脆(三段缓动的活);中途断网重试看进度是否倒退(单调锁的活);掐表看 100% 之后一秒内是否切结果(终态闭环的活);开读屏听播报是否按台阶走(无障碍的活)。五步都有数,这个进度组件就算合格。别小看这五步——历史上全部的进度事故,拆开就是这五步里某一步的缺位。

十四、总结:进度体系四条军规

收束四条:一、类型分明——不知道进度就转圈,不装确定;二、感知优化——三段缓动映射真实值,单调锁防回退;三、终态闭环——100% 有完成动画即切结果,失败有红条加重试,不留悬空态;四、无障碍有节——步进播报+终态播报,装饰性进度对读屏静默。Progress 是最简单的组件之一,但它承载的是用户等待时的全部情绪——节奏顺、终态明、不回退,等待就成了可接受的过渡,而产品与用户之间的信任,正是在这一次次可接受的过渡里攒出来的。

参考资料:

  • 华为开发者:Progress 进度条组件
  • 华为开发者:animateTo 与曲线
  • 华为开发者:无障碍 accessibilityText

#鸿蒙 #HarmonyOS 7(API 26)

Logo

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

更多推荐