HarmonyOS 7 闪控窗排队信息分层与轻操作:连载04(共08篇)【共创稿事节】

闪控球适合持续呈现一两项关键字段,但用户临近展厅入口时,往往需要同时确认活动名称、当前序号、预计等候时间以及入场凭证入口。如果让这些信息全部挤进简短标题,系统提供的固定模板就会失去可读性。HarmonyOS 7 的闪控窗允许应用加载专门的 ArkUI 内容页,在独立的小窗口里组织信息层级和少量操作,适合把排队摘要扩展为可直接使用的状态卡片。

上一篇已经用 floatingBall 展示排队简要状态。这一篇继续使用同一组 EventInfo、EntryPass 和 QueueSnapshot,完成闪控窗的内容页、操作分区和启动桥接。核心约束仍然是:排队阶段由业务归约器决定,窗口负责把结果呈现给用户;刷新请求和返回凭证动作应进入已有业务链路,不应在窗口里自行更改排队资格。

闪控窗排队状态与操作区域设计

一、把窗口分成四个有明确职责的区域

对于一张城市艺术夜展凭证,小窗口内最重要的信息通常是“我参加什么活动”“现在排到哪里”“接下来要做什么”。将它们从上往下安排,可以避免用户反复寻找叫号状态。设计上建议把活动名称与阶段放在首屏,序号作为最大字号,等待时间紧随其后,再提供刷新与查看凭证的操作。

区域内容表达原则
标题区活动名称、当前阶段文案短、状态明确
核心进度区当前序号、预计等待时间数字突出,不以颜色作为唯一依据
操作区刷新进度、查看入场凭证控制在两项常用动作以内
辅助信息区场馆入口、数据更新时间辅助判断信息是否过期

小窗口与完整页面不是尺寸缩放的关系。主页面有活动封面、路线、场馆介绍和历史凭证;闪控窗只保留当前这一张入场凭证的相关信息。多张凭证并存时,需要先由用户在主页面确定关注对象,再用稳定的 ticketId 形成窗口投影,不能用活动列表的第一行代替用户选择。

当业务阶段从 WAITING 进入 CALLED,最重要的信息应从序号改为“请前往入口”;到达 ADMITTED 后,则不再突出等候时间。处理方式是先生成一个适合窗口的投影对象,再由页面做条件渲染,而不是在每个 Text() 组件里重复编写 switch 语句。

二、复用排队快照构建窗口投影

在 logic/QueueFloatProjection.ets 中,把连载02关联后的活动资料与连载01的排队快照转为 QueueFloatPanel。该对象只包含界面必要字段,不保存核验密钥,不负责修改原始业务记录。为了方便分辨数据是否过期,投影中保留原始 seq 和可展示的更新时间文本。

import { EntryPhase, QueueSnapshot, phaseLabel } from '../model/QueueModel';
import { EventInfo, EntryPass } from '../model/EventCatalog';

export interface QueueFloatPanel {
  ticketId: string;
  seq: number;
  eventTitle: string;
  phaseText: string;
  positionText: string;
  waitText: string;
  venueText: string;
  updatedText: string;
  canOpenPass: boolean;
}

export function toFloatPanel(event: EventInfo, pass: EntryPass,
  snapshot: QueueSnapshot): QueueFloatPanel {
  const matches: boolean = pass.ticketId === snapshot.ticketId &&
    pass.eventId === snapshot.eventId && event.eventId === pass.eventId;
  const waiting: boolean = snapshot.phase === EntryPhase.WAITING;
  const called: boolean = snapshot.phase === EntryPhase.CALLED;
  return {
    ticketId: pass.ticketId,
    seq: snapshot.seq,
    eventTitle: event.title,
    phaseText: matches ? phaseLabel(snapshot.phase) : '进度待更新',
    positionText: matches && waiting ? `${snapshot.position}` : (matches && called ? '请前往入口' : '—'),
    waitText: matches && waiting ? `预计等待${snapshot.waitMinutes}分钟` : '查看最新入场信息',
    venueText: event.venue,
    updatedText: matches ? new Date(snapshot.updatedAt).toLocaleTimeString() : '等待新数据',
    canOpenPass: matches
  };
}

这个函数仍然执行活动、凭证和快照的三方匹配。窗口的展示内容可以精简,但身份校验不能精简。当匹配失败时,页面保留活动资料并提示“进度待更新”,避免把其他凭证的序号拼到当前活动里。终止阶段用清楚的状态文字覆盖进度区域,用户能够理解本次入场记录已经发生变化。

投影里的时间字符串由界面格式化。如果应用面向跨地区用户,还应结合活动所属时区决定时间文案。序号是队列提供的字段,等候分钟数来自快照,不应由窗口内一个单独的定时器自行递减。用户即使看到估计时间接近零,也仍需等待业务事件确认入场阶段变化。

三、ArkUI 内容页实现层级布局

闪控窗可以通过 setUIContext() 加载工程内的页面,页面路径必须登记在 resources/base/profile/main_pages.json 中。相比直接把主页面搬入窗口,新增 pages/FloatViewPage.ets 更容易控制字重、留白和按钮尺寸。这里采用 @LocalStorageLink 接收启动时传入的窗口数据,并将点击动作交给业务入口转发层。

QueueFloatActions 是应用自身的轻操作注册器,包含 onRefresh 和 onOpenPass 两个回调,后面由窗口控制器绑定实现;它不对应系统 API。

// logic/QueueFloatActions.ets
export class QueueFloatActions {
  static onRefresh: (() => void) | undefined;
  static onOpenPass: (() => void) | undefined;
}

pages/FloatViewPage.ets:

import { QueueFloatPanel } from '../logic/QueueFloatProjection';
import { QueueFloatActions } from '../logic/QueueFloatActions';

@Entry({ useSharedStorage: true })
@Component
struct FloatViewPage {
  @LocalStorageLink('queuePanel') panel: QueueFloatPanel = {
    ticketId: '', seq: 0, eventTitle: '活动入场',
    phaseText: '进度待更新', positionText: '—',
    waitText: '查看最新入场信息', venueText: '',
    updatedText: '', canOpenPass: false
  };

  build() {
    Column({ space: 12 }) {
      Row() {
        Text(this.panel.eventTitle)
          .fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1)
        Text(this.panel.phaseText).fontSize(14).fontColor('#2563EB')
      }.width('100%')

      Column({ space: 6 }) {
        Text('当前排队信息').fontSize(13).fontColor('#64748B')
        Text(this.panel.positionText)
          .fontSize(35).fontWeight(FontWeight.Bold).fontColor('#1348A8')
        Text(this.panel.waitText).fontSize(15)
      }.width('100%').padding(14)
       .backgroundColor('#EEF5FF').borderRadius(16)

      Text(this.panel.venueText).fontSize(13).fontColor('#475569')
      Row({ space: 10 }) {
        Button('刷新进度').layoutWeight(1)
          .onClick(() => QueueFloatActions.onRefresh?.())
        Button('查看凭证').layoutWeight(1)
          .enabled(this.panel.canOpenPass)
          .onClick(() => QueueFloatActions.onOpenPass?.())
      }.width('100%')
      Text(`更新于 ${this.panel.updatedText}`)
        .fontSize(12).fontColor('#64748B')
    }.width('100%').padding(16)
     .backgroundColor(Color.White)
  }
}

这段布局把主状态放在同一个背景容器中,让较小尺寸下的阅读顺序更稳定。两个按钮不设置自动提交排队信息的行为;“刷新进度”向业务层发起重新获取快照的请求,“查看凭证”恢复主窗口,再由主窗口根据 ticketId 定位凭证详情。这样既能减少窗口内操作的复杂度,也可以让凭证详情沿用原来的安全校验流程。

如果文案长度超过预期,优先压缩场馆地址的展示长度,保留完整阶段和操作按钮。不能为了让一个数字更醒目而隐藏失效状态,也不能把“已取消”显示为灰色但继续保留可点击的入场操作。

四、创建闪控窗与注入窗口数据

HarmonyOS 7(API 26)的 floatView 提供 isFloatViewEnabled()、create()、getFloatViewLimits() 以及 FloatViewController 的 setUIContext()、setWindowSize()、start() 等接口。窗口采用 Stage 模型,使用前需在模块配置中声明 ohos.permission.FLOAT_VIEW,并按照用户授权流程取得权限。闪控窗与已启动的闪控球、画中画存在互斥关系,从上篇的球切换到窗口前,需要先完成停止和状态清理。

下面的网关只负责创建窗口和连接 UI 内容,业务状态仍由原有模型负责。LocalStorage 从 toFloatPanel() 创建初始值,再交给独立页面读取。窗口标题栏和系统级拖动行为由窗口能力管理,页面无需自行编写一套拖拽手势。

import { floatView } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { EventInfo, EntryPass } from '../model/EventCatalog';
import { QueueSnapshot } from '../model/QueueModel';
import { toFloatPanel } from './QueueFloatProjection';
import { QueueFloatActions } from './QueueFloatActions';

export class QueueFloatGateway {
  private controller?: floatView.FloatViewController;
  private storage?: LocalStorage;
  private started: boolean = false;

  async open(ctx: common.UIAbilityContext, event: EventInfo,
    pass: EntryPass, snapshot: QueueSnapshot,
    refresh: () => void): Promise<boolean> {
    if (!canIUse('SystemCapability.Window.SessionManager') ||
      !floatView.isFloatViewEnabled()) return false;
    try {
      if (this.controller) return this.started;
      const config: floatView.FloatViewConfiguration = {
        context: ctx,
        templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE
      };
      const current = await floatView.create(config);
      this.controller = current;
      this.storage = new LocalStorage({
        queuePanel: toFloatPanel(event, pass, snapshot)
      });
      current.onStateChange((info: floatView.FloatViewStateChangeInfo) => {
        this.started = info.state === floatView.FloatViewState.STARTED;
        if (info.state === floatView.FloatViewState.STOPPED) {
          current.offStateChange();
          QueueFloatActions.onRefresh = undefined;
          QueueFloatActions.onOpenPass = undefined;
          this.storage = undefined;
          if (this.controller === current) this.controller = undefined;
        }
      });
      QueueFloatActions.onRefresh = refresh;
      QueueFloatActions.onOpenPass = (): void => {
        const params: Record<string, Object> = { ticketId: pass.ticketId };
        current.restoreMainWindow(params)
          .catch((e: Error) => console.error(`返回凭证页失败:${e.message}`));
      };
      await current.setUIContext('pages/FloatViewPage', this.storage);
      const limits = floatView.getFloatViewLimits(
        floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
      await current.setWindowSize(limits.maxSize);
      await current.start();
      return true;
    } catch (e) {
      console.error(`闪控窗开启失败:${String(e)}`);
      this.controller?.offStateChange();
      this.controller = undefined;
      this.storage = undefined;
      QueueFloatActions.onRefresh = undefined;
      QueueFloatActions.onOpenPass = undefined;
      return false;
    }
  }
}

setWindowSize() 的尺寸使用系统返回的像素范围,而 ArkUI 页面布局中的数值通常按逻辑布局单位理解,不能混成一套固定值。系统窗口还可能因为设备形态变化改变尺寸,后续可以通过尺寸相关回调适配紧凑布局。上例的 start() 完成仅代表启动请求得到处理,后续需观察状态事件中的 STARTED;STOPPED 到达时也应释放按钮回调和数据引用,避免旧窗口的处理函数被新窗口沿用。

闪控窗内容页与开发环境布局

五、窗口操作要与业务动作严格分离

“刷新进度”只是一次主动拉取请求。即使网络返回了更旧的进度,也必须先经过连载01的 reduceQueue() 才能更新界面。对于快速连续点击,可以在请求层设置单飞控制:已有刷新请求尚未完成时,复用当前请求或暂时禁用按钮,防止同一凭证产生多路竞争更新。按钮反馈应区分“正在刷新”“暂无新变化”和“获取失败”。

“查看凭证”则更需要避免越权。控制器可以在用户点击窗口后调用 restoreMainWindow(),向主窗口传递 ticketId。主窗口接到参数后,必须根据本地会话和服务端凭证归属重新定位目标,而不是把这个字符串当成已通过核验的授权结果。闪控窗仅承担跳转意图,凭证详情与核验逻辑仍留在完整应用页面。

活动入场排队闪控窗的手机界面

六、结合窗口状态排查常见异常

异常表现重点位置建议处理
点击开启后看不到窗口支持状态、授权结果与状态回调先检查 STARTED 是否到达
窗口创建后立即失败页面路径、模板与尺寸对齐 main_pages.json 和尺寸范围
与闪控球切换时出错两种窗口是否仍然同时活动等前一个窗口完成停止再启动
点击刷新但数字不变是否收到更新版本、是否被归约器拒绝对比 ticketId 与 seq
关闭窗口后按钮仍响应是否释放窗口动作注册器在停止回调中清理持有关系
凭证页打开了错误活动是否只凭列表下标定位按凭证 ID 再查活动归属

这类问题应优先从“业务事件版本 → 投影结果 → LocalStorage 中的字段 → 窗口生命周期”逐层追踪。若业务快照本身旧,调高界面刷新频率无济于事;若快照已新而窗口仍旧,则应关注投影替换和跨 UI 内容的状态传递。日志建议记录阶段、版本和错误码即可,避免输出完整票证或个人信息。

七、把扩展留在明确的层次

这一阶段形成了完整的闪控窗基本形态:原有活动和凭证实体提供身份数据,排队快照提供进度事实,投影层组织窗口文案,ArkUI 页面负责视觉层级,floatView 控制器负责窗口创建和生命周期。后续加入高频进度消息时,只需要让新的投影结果进入 LocalStorage,并通过窗口状态控制刷新节奏,无须重新设计入场凭证和排队规则。

闪控球和闪控窗分别满足“快速扫一眼”和“完成少量操作”的需要。它们可以共享相同的业务状态,但不应拥有彼此独立的排队计算逻辑。保持一个事实来源、一个当前关注凭证、清楚的窗口切换次序,才能让轻量交互在业务状态不断变化时依然可靠。

参考资料:HarmonyOS 7 新能力一览、闪控窗开发指导、闪控窗 API 参考。

Logo

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

更多推荐