HarmonyOS 7闪控窗与闪控球连载04(共08篇):触控操作与任务进度反馈设计【共创稿事节】
轻任务表面越小,用户对反馈的容忍度就越低。一个圆形入口可能只展示任务名称和进度,展开后的轻量窗口则需要暂停、继续、取消等操作。如果点击后界面没有变化,用户容易重复触发;如果进度条能够随意拖到100%,又会把操作意图误写成真正的任务完成情况。解决问题的关键不是增加动画,而是把“触控指令”“任务执行状态”“可视反馈”分成三条清晰的数据链。
前面已经搭建轻任务队列、窄窗与宽窗布局以及球窗切换状态管理。本篇在这些结构上补充任务操作层:同一个任务在不同表面上具有相同的操作结果,所有进度必须来自任务执行事件,界面只能显示或发送命令,不能直接篡改业务完成率。系统级闪控窗和闪控球的接入形式应按目标设备和公开能力确认;这里的交互层采用常规ArkUI组件和普通业务服务,可用于应用内轻任务界面。

一、任务进度和触控反馈不是同一个字段
以“素材批量整理”为例,一项任务包含总文件数、已处理数、当前阶段和最近更新时间。计算进度应基于业务事实,而非控件位置。用户的点击会产生pause、resume、cancel等命令,命令提交后可以进入“处理中”反馈,但只有执行服务返回新状态,才更新任务阶段。这样即使窗口从紧凑入口展开,任务的完成率也不会突然跳变。
| 信息 | 数据来源 | 界面用途 |
|---|---|---|
| 已处理数量 | 执行服务进度事件 | 计算实际完成率 |
| 当前阶段 | 服务状态机 | 决定操作按钮是否可用 |
| 正在提交的命令 | 交互控制层 | 防止重复点击与显示忙碌状态 |
| 控件触摸状态 | ArkUI事件回调 | 提供按压、拖动等即时反馈 |
| 最近事件版本 | 任务仓库 | 丢弃顺序落后的更新 |
如果一项任务包含多个阶段,应先明确进度的业务含义:是按文件计数,还是按权重计算。不要在上传阶段使用字节进度、在处理阶段直接重置为零,否则用户看到的整体进度会倒退。对于阶段权重,定义固定映射并在任务开始时确定,避免根据实时耗时任意调整权重。
二、使用可比较的状态与事件版本
阶段状态以显式枚举表示,指令以字符串联合或枚举限制可选范围。任务仓库负责状态演进的合法性,UI从仓库拿快照。对同一任务,每个有效服务事件携带递增版本,旧事件晚到时直接忽略。
export enum TaskPhase {
QUEUED = 'queued',
RUNNING = 'running',
PAUSED = 'paused',
DONE = 'done',
FAILED = 'failed',
CANCELLED = 'cancelled'
}
export type TaskCommand = 'pause' | 'resume' | 'cancel'
export interface TaskSnapshot {
id: string
label: string
phase: TaskPhase
processed: number
total: number
revision: number
}
export interface ProgressEvent {
id: string
processed: number
total: number
phase: TaskPhase
revision: number
}
export function toPercent(task: TaskSnapshot): number {
const total = Math.max(1, task.total)
const done = Math.max(0, Math.min(task.processed, total))
return Math.floor(done * 100 / total)
}
toPercent对异常分母做保护,且把已处理数量夹在合法范围内。这是视图层的数据保护,不意味着后端可以传入任意负数。仓库接收事件时还需校验任务存在、版本递增、数量非负与状态流转。例如已经取消的任务收到较旧的运行事件,不应回到运行中。
三、命令网关负责去重,而不是按钮自身
应用内多个任务表面可能同时存在。球态与窗态上的暂停按钮看起来不同,但必须走同一个命令网关。命令提交中将对应任务标记为忙碌,防止用户连续点击制造多个相同请求。服务确认后再清除忙碌标记;失败时恢复按钮并给出可理解的错误原因。
export interface TaskGateway {
send(taskId: string, command: TaskCommand): Promise<void>
}
export class TaskCommandController {
private pending: Set<string> = new Set<string>()
constructor(private gateway: TaskGateway) {}
isBusy(taskId: string): boolean {
return this.pending.has(taskId)
}
async issue(taskId: string, command: TaskCommand): Promise<boolean> {
if (this.pending.has(taskId)) {
return false
}
this.pending.add(taskId)
try {
await this.gateway.send(taskId, command)
return true
} finally {
this.pending.delete(taskId)
}
}
}
这里的Set完成进程内并发去重。如果网络重试或进程恢复可能再次提交命令,服务端还应采用稳定的请求幂等键,单纯禁用按钮不足以覆盖跨进程重复提交。界面里issue的返回值表示命令是否被成功提交,而不是表示任务已经暂停或取消;任务最终阶段仍以执行服务返回为准。
四、进度只读,触控负责发出意图
对于任务完成率,Progress比Slider更符合语义。前者显示执行服务的结果,后者允许用户调节一个值。若业务支持手动调整执行优先级,可以单独放置一个Slider表示优先级,例如设置0到100的调度权重;不要把它与完成率共用状态字段。华为ArkUI的Slider.onChange会报告SliderChangeMode,能够区分拖动中、结束和点击轨道。可以在拖动中更新草稿值,在结束或点击时提交配置,从而避免每个移动事件都发起服务请求。
@Component
struct TaskOperationCard {
@Prop task: TaskSnapshot
@State private priorityDraft: number = 50
@State private isSending: boolean = false
private commitPriority(value: number): void {
// 接入应用的配置服务,优先级与任务完成率分别存储。
console.info(`priority changed: ${Math.round(value)}`)
}
build() {
Column({ space: 12 }) {
Row() {
Text(this.task.label).fontSize(18).fontWeight(FontWeight.Medium)
Blank()
Text(`${toPercent(this.task)}%`).fontSize(16)
}
Progress({ value: toPercent(this.task), total: 100, type: ProgressType.Linear })
.width('100%')
Text('执行优先级').fontSize(13).fontColor('#64748B')
Slider({ value: this.priorityDraft, min: 0, max: 100, step: 5 })
.onChange((value: number, mode: SliderChangeMode) => {
this.priorityDraft = value
if (mode === SliderChangeMode.End || mode === SliderChangeMode.Click) {
this.commitPriority(value)
}
})
Row({ space: 12 }) {
Button('暂停').enabled(this.task.phase === TaskPhase.RUNNING && !this.isSending)
Button('继续').enabled(this.task.phase === TaskPhase.PAUSED && !this.isSending)
}
}.padding(16).borderRadius(18).backgroundColor('#FFFFFF')
}
}
组件保留了按钮可用性判定的位置,实际接入时让按钮调用上层注入的命令控制器,并通过同一个isSending状态刷新操作表面。ProgressType.Linear展示的百分比来自任务快照;即便用户拖动优先级滑块,也不会改变完成率。若优先级配置服务不能实时调整,则使用“保存设置”按钮确认提交,而非默认每次滑动结束都立即生效。

五、危险操作需要明确的确认过程
轻量窗口上的取消按钮占用空间有限,但取消可能导致当前工作流程中止。可采用长按动作进入确认状态,也可以点击后打开明确的确认弹层。LongPressGesture允许设置触发时间。设计时不要将长按当成唯一入口,应同时考虑键盘、无障碍操作和用户对长按手势的不熟悉程度。
@Component
struct CancelTaskAction {
@State private askConfirm: boolean = false
build() {
Column() {
Button('取消当前任务')
.backgroundColor('#FEE2E2')
.fontColor('#991B1B')
.onClick(() => { this.askConfirm = true })
.gesture(LongPressGesture({ duration: 650 })
.onAction(() => { this.askConfirm = true }))
if (this.askConfirm) {
Text('确定结束当前处理任务?').margin({ top: 12 })
Row({ space: 12 }) {
Button('返回').onClick(() => { this.askConfirm = false })
Button('确认取消').onClick(() => {
this.askConfirm = false
// 通过统一命令网关提交取消请求。
})
}
}
}
}
}
本段将确认过程保留在组件状态内,适合表达交互流程。需要与系统弹层保持一致时,可使用受支持的对话框组件,避免小面积弹层遮住关键进度信息。对于已经完成或已取消的任务,取消入口应变为禁用或转为“查看结果”,不能重复发送状态转换命令。
六、节流更新与前后台反馈
任务可能每几十毫秒发来一个计数事件。将所有事件逐条映射成组件树更新,会增加轻量窗口的刷新负担。仓库应完整接收业务事件、保留最新快照,视图层再按需要合并展示更新:只有进度整数百分比、阶段文字或重要提示改变时才触发对应UI刷新。这不是丢弃业务进度,而是将存储频率与绘制频率解耦。
如果界面暂时不可见,订阅端可以继续维护最新快照;重新打开时读取仓库当前值,而不是重放每一条旧事件制造跳动。球态可以只保留进度环、简短状态和展开入口,窗态展示文件数量、按钮与最近事件说明,两种表面从同一个快照派生输出。失败状态要体现可以重试的操作条件,网络异常时不要把“正在连接”误写为“已完成”。
轻量交互还应保留视觉以外的反馈:操作提交期间禁用重复按钮,任务完成时更新语义文字,重要失败提示使用可访问的文本,不把状态只编码为颜色。颜色、进度环与触控震动只能补充信息,不能替代明确状态。

七、按事件顺序排查问题
重复暂停。 先检查按钮状态,再看命令网关是否在请求开始前登记忙碌标记。若多个表面分别实例化网关,将产生各自的锁;此时需要把控制器提升到共享层或在服务端使用幂等键。
进度倒退。 核对服务事件的revision和processed;界面只接受版本更新的快照。对于任务进入新阶段时计数口径变化的情况,应调整进度计算模型,不要用Math.max掩盖业务语义错误。
滑块触发过量请求。 检查配置提交是否被放在Moving分支。如果只需要拖动结束后保存,可将请求移至End与Click,并保留草稿值显示,从而减少不必要的重复调用。
窗口重新展开后按钮状态错误。 检查是否把isSending作为局部组件字段,在销毁后丢失。提交状态和快照订阅应由共享控制器提供;重新展开时根据真实阶段与待处理命令推导按钮状态。
验收时可分别测试快速连点、长按后放弃、进度事件乱序、命令超时后重试、紧凑与展开表面切换,以及任务完成瞬间收到取消指令。关注状态收敛和界面可操作性,而不是用未经测量的响应时间数字替代判断。
这一篇将交互操作与执行进度分离:触控产生命令,服务更新任务,仓库协调事件,表面选择合适的反馈密度。后续跨页面状态同步只需要保持这一数据链唯一,便能减少球窗切换引发的误操作。
更多推荐

所有评论(0)