正在上传一份体积较大的设计文件,用户却临时切到日程、聊天或文档应用。文件传输还在进行,用户只想知道当前进度、有没有报错、能不能暂停。他不需要为了确认一个百分比,反复返回完整的文件管理页面。HarmonyOS 7 的闪控窗与闪控球正是围绕这类跨应用轻量交互提出的系统能力:闪控窗适合持续展示服务状态,闪控球承担更紧凑的入口形态,两者之间可以切换。

技术方案不能从“先做一个漂浮按钮”开始。真正难以处理的是后台任务状态、界面动作和业务结果保持一致。窗口可以随时收起,任务本身却仍在变化;暂停操作与完成回调也会发生竞态。本文用文件上传的业务场景构建一条可靠的轻量交互链路,并将业务逻辑与承载界面的实现分开,从而让状态管理不会依赖某一种窗口样式。

闪控窗与闪控球的文件任务交互结构

界面只有两种形态,状态却不能只有两种

对于文件上传,紧凑形态可以只包含图标、进度与异常提示;展开形态则增加文件名、剩余任务数量、暂停、继续和返回应用等动作。无论切换哪一种形态,业务状态都应来自同一个上传任务对象。窗口自身的显示与隐藏属于界面层事件,不等于业务任务开始与结束。

例如传输完成后,控制层仍可能收到晚到的进度事件。如果界面直接把最后一条事件当成权威结果,就会出现“已完成又变回进行中”的倒退。应为任务定义明确的状态机,并对非法迁移做限制。在上传中可以暂停、取消或者进入完成状态;进入完成状态后,普通进度回调只能被忽略。失败后的重试则必须创建新的尝试标识,以免旧请求的回调覆盖新任务。

下方代码将任务模型与状态转换集中在一个控制器中。attempt 是本次上传尝试的序号,它能帮助识别迟到的网络事件。传输 SDK 的回调进入控制器之前,应当先完成参数检查和线程调度,再更新 UI 所订阅的快照。

enum UploadPhase {
  Idle = 'idle', Running = 'running', Paused = 'paused',
  Done = 'done', Failed = 'failed'
}

interface UploadSnapshot {
  id: string;
  fileName: string;
  phase: UploadPhase;
  progress: number;
  attempt: number;
}

class UploadTaskController {
  private snapshot: UploadSnapshot = {
    id: 'report', fileName: '季度设计文件.pdf',
    phase: UploadPhase.Idle, progress: 0, attempt: 0
  };

  start(): UploadSnapshot {
    this.snapshot = {
      ...this.snapshot, attempt: this.snapshot.attempt + 1,
      phase: UploadPhase.Running, progress: 0
    };
    return this.read();
  }

  onProgress(attempt: number, percent: number): UploadSnapshot {
    if (attempt !== this.snapshot.attempt || this.snapshot.phase !== UploadPhase.Running) {
      return this.read();
    }
    const next = Math.max(this.snapshot.progress,
      Math.min(100, Math.max(0, Math.round(percent))));
    this.snapshot.progress = next;
    return this.read();
  }

  pause(): UploadSnapshot {
    if (this.snapshot.phase === UploadPhase.Running) {
      this.snapshot.phase = UploadPhase.Paused;
    }
    return this.read();
  }

  resume(): UploadSnapshot {
    if (this.snapshot.phase === UploadPhase.Paused) {
      this.snapshot.phase = UploadPhase.Running;
    }
    return this.read();
  }

  complete(attempt: number): UploadSnapshot {
    if (attempt === this.snapshot.attempt && this.snapshot.phase === UploadPhase.Running) {
      this.snapshot.phase = UploadPhase.Done;
      this.snapshot.progress = 100;
    }
    return this.read();
  }

  read(): UploadSnapshot {
    return { ...this.snapshot };
  }
}

这里的 pause() 只是状态机的决策入口,实际传输暂停还需要由上传服务完成并确认结果。工程中应由服务操作成功后再推送新快照,不应仅通过点击按钮便认定传输已暂停。上传完成也应以服务器确认、文件校验或业务协议规定的成功条件为准,不能只凭进度等于 100 判断结果。状态机的价值就在于让界面层只呈现可解释的结果,而不替网络层做决定。

将轻量交互拆成可替换的三个接口

推荐把结构划分为任务仓库、动作调度器和窗口呈现器。任务仓库存储最新快照,能够为主页面和轻量窗口提供一致数据;动作调度器接收暂停、继续、取消等操作请求,并保证重复点击具备幂等性;窗口呈现器将快照转换为球形入口或展开面板,而不会直接修改网络任务状态。这样即便某个窗口被收起,业务服务仍然能够保持独立运行。

下面给出一段只依赖业务接口的 ArkTS 结构代码。ControlSurface 描述 UI 需要的最小数据,ControlBridge 是接入业务服务时的适配层。它们既可以供应用主页面使用,也能在后续按目标系统版本的接入规范连接到相应的窗口承载方式,而不需要把核心状态机重写。

interface ControlSurface {
  title: string;
  percent: number;
  message: string;
  canPause: boolean;
  canResume: boolean;
}

class ControlPresenter {
  toSurface(task: UploadSnapshot): ControlSurface {
    return {
      title: task.fileName,
      percent: task.progress,
      message: this.phaseText(task.phase),
      canPause: task.phase === UploadPhase.Running,
      canResume: task.phase === UploadPhase.Paused
    };
  }

  private phaseText(phase: UploadPhase): string {
    switch (phase) {
      case UploadPhase.Running: return '传输进行中';
      case UploadPhase.Paused: return '已暂停';
      case UploadPhase.Done: return '传输完成';
      case UploadPhase.Failed: return '传输失败';
      default: return '等待开始';
    }
  }
}

interface ControlBridge {
  current(): Promise<UploadSnapshot>;
  pause(taskId: string): Promise<UploadSnapshot>;
  resume(taskId: string): Promise<UploadSnapshot>;
}

为了降低主页面和轻量窗口之间的数据耦合,快照最好包含单调递增的版本字段或服务端更新时间。接收端仅应用比当前版本更新的数据:对于同一个任务,版本小的消息直接丢弃;切换到新一次上传尝试时清空旧进度;退出登录后立即撤销与该账号关联的界面状态。若产品涉及多个并发上传任务,还需定义优先级,例如“失败待处理”优先于“低优先级任务的普通进度”。这些规则先由业务层决定,再交给窗口承载层渲染,能够避免界面争夺控制权。

上传状态、操作接口与轻量窗口的分层结构

应用内浮层可以先把交互规则跑通

在应用自身的窗口里,ArkUI 的 bindPopup 能为按钮提供轻量信息面板,适合复用上述任务卡片的视觉结构。它和跨应用的系统闪控窗是不同的承载方式,界面代码不应因此硬绑定系统窗口的创建和管理逻辑。下列页面通过 @State 管理面板开合,点击“查看当前任务”显示一个带进度和暂停入口的任务视图。业务操作可在 onClick 中接入前面的 ControlBridge。

@Entry
@Component
struct TransferHome {
  @State panelOpen: boolean = false;
  @State percent: number = 68;

  @Builder
  taskPanel() {
    Column({ space: 12 }) {
      Text('季度设计文件.pdf')
        .fontSize(18).fontWeight(FontWeight.Medium)
      Text(`已传输 ${this.percent}%`).fontSize(14)
      Progress({ value: this.percent, total: 100 })
        .color('#276CE5')
      Row({ space: 12 }) {
        Button('暂停').onClick(() => {
          // 这里连接业务服务的暂停动作,完成后再刷新状态
        })
        Button('返回任务').onClick(() => { this.panelOpen = false; })
      }
    }
    .width(270).padding(16)
    .backgroundColor('#FFFFFF').borderRadius(16)
  }

  build() {
    Column({ space: 20 }) {
      Text('文件传输中心').fontSize(26)
      Button('查看当前任务')
        .onClick(() => { this.panelOpen = true; })
        .bindPopup(this.panelOpen, {
          builder: this.taskPanel,
          placement: Placement.Bottom,
          onStateChange: (event) => {
            this.panelOpen = event.isVisible;
          }
        })
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这块 UI 重点验证的是触达与信息密度:收起后用户是否清楚任务仍在继续,展开后暂停与继续是否相互排斥,报错时是否提供足够明确的下一步动作。悬浮入口不应遮挡当前应用中最重要的提交按钮;展示时优先只呈现文件名、进度和状态,详细日志、历史记录和复杂设置仍交由完整应用页面处理。

手机文件传输页面与轻量任务面板

跨应用轻量窗口还要考虑这些条件

系统级闪控窗涉及自身的窗口显示、拖动、暂存和与闪控球的形态切换;界面状态机并不能替代系统侧的接入流程。正式接入时,需要按目标 SDK 和开发者文档确认对应接口、窗口权限、适配条件以及生命周期约束。尤其是用户主动隐藏或结束任务时,应用应该尊重这个选择,避免通过频繁弹出窗口争夺焦点,也要避免把敏感文件名、客户资料或私密通知长期展示在其他应用上层。

性能方面,进度百分比不必每收到一小段字节便刷新一遍。若服务每秒产生数十个进度事件,可以对视觉更新做节流,在任务完成、失败、暂停等重要状态上立即刷新;UI 层也应优先传递轻量快照,而不是把整个文件列表和网络对象塞进窗口。对于错误状态,保留“查看原因”和“返回应用”的入口比反复弹出错误提示更加稳妥。用户需要的是一次可完成的操作,不是一组持续打扰的通知。

把闪控窗和闪控球放到工程视角,核心不是“窗口漂浮起来”这一视觉动作,而是同一个任务跨越不同界面形态,依然具有可追踪、可取消、可恢复的状态。做到这一点,业务逻辑才能支撑从小球提醒到展开控制的完整闭环,后续功能扩展也无需把进度管理重新写一遍。

文件传输还有一个容易忽略的产品规则:隐私优先级高于信息密度。即使界面空间足够,也不要在浮层里持续展示完整的敏感文件路径或账户信息。窗口折叠到最小形态后,展示“正在处理 1 项任务”通常就够了;点击后再根据用户当前授权状态决定是否展开细节。发生登录失效、文件被撤回或者应用切换账号时,控制器必须清理旧快照,避免残留操作继续作用在已经失效的业务对象上。

操作层还应避免把一次点击直接绑定到长耗时请求的每个阶段。用户快速点击两次暂停按钮时,可以暂时禁用重复提交,待服务返回确认结果后再恢复操作。这种处理方式不依赖窗口样式,能够在应用页面、任务通知和其他轻量入口之间复用。它使用户看到的每个按钮都对应清楚的动作结果,而不是一组无法预测的网络调用。

相关官方资料:HarmonyOS 7 新能力一览:闪控窗、应用主窗与辅助窗口生命周期、ArkUI bindPopup 交互说明、ArkUI OverlayManager。

Logo

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

更多推荐