HarmonyOS 7 + ArkUI floatView:闪控窗启动回调与重复操作的状态门禁【鸿蒙心迹】
示例工程:FocusRelay|主页面
FocusMainPage|闪控页FocusFloatPage|示例会话FV-051> 说明:本文代码为基于官方接口签名整理的工程示例,日志、页面、状态均是用于讲解的预期演示数据,不代表已经完成真机调试。在专注计时器里,悬浮内容不是难做的部分。更棘手的是用户连续点击“开启闪控窗”,第一次请求刚发出,第二次就到了;启动 Promise 已返回,窗口却仍可能没有进入可见状态。如果把 Promise 完成理解为“窗口已出现”,主页面很容易过早启用“结束窗口”按钮。FocusRelay 只维护一项任务:FV-051,计划时长二十五分钟。先把窗口控制的边界收紧,暂不接后台计时同步,也不尝试让任意后台服务唤醒悬浮窗口。目标是将“请求已发送”“系统确认已显示”“用户已停止”分成不同状态,并让每条错误都能在日志里找到对应操作。## 一、真正要判断的是哪一个完成时刻官方
floatView模块用于闪控窗管理,导入路径为@kit.ArkUI,参考接口从 API 26.0.0 起提供,限定 Stage 模型。isFloatViewEnabled()判断当前设备是否具备对应能力,不能替代窗口权限检查、主窗口前台状态,也不能保证创建一定成功。创建时还需要有效的UIAbilityContext。启动接口start()返回 Promise,但官方明确指出:应以onStateChange收到FloatViewState.STARTED作为窗口真正启动的依据。停止动作同理,stop()返回后仍需等待STOPPED。这两个时间点之间,按钮就应该处于不可重复操作状态。我为业务界面定义IDLE / PREPARING / STARTING / STARTED / STOPPING / STOPPED / FAILED七个状态。它们是 FocusRelay 自己的显示状态,不是平台新提供的枚举。通过把“命令态”和“系统态”区分开,用户看到的文字才不会领先于实际窗口。### 1. 先创建控制器,再挂载内容这段代码解决两个问题:设备不支持时提前退出;主窗口里的内容路径必须与页面清单登记一致。它只示范控制器初始化,不把初始化成功误写成窗口启动完成。arktsimport { common } from '@kit.AbilityKit';import { floatView } from '@kit.ArkUI';type FocusGateState = 'IDLE' | 'PREPARING' | 'STARTING' | 'STARTED' | 'STOPPING' | 'STOPPED' | 'FAILED';class FocusFloatGate { private controller?: floatView.FloatViewController; private phase: FocusGateState = 'IDLE'; private busy: boolean = false; private onPhase: (value: FocusGateState) => void; constructor(onPhase: (value: FocusGateState) => void) { this.onPhase = onPhase; } private setPhase(next: FocusGateState): void { this.phase = next; this.onPhase(next); console.info(`FocusRelay/FV-051 phase=${next}`); } async prepare(ctx: common.UIAbilityContext): Promise<boolean> { if (this.busy || this.controller) return false; if (!floatView.isFloatViewEnabled()) { this.setPhase('FAILED'); return false; } this.busy = true; this.setPhase('PREPARING'); try { const config: floatView.FloatViewConfiguration = { context: ctx, templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE }; this.controller = await floatView.create(config); await this.controller.setUIContext('pages/FocusFloatPage'); this.controller.onStateChange(this.onFloatState); return true; } catch (err) { this.setPhase('FAILED'); console.error(`FocusRelay/FV-051 prepare failed ${JSON.stringify(err)}`); return false; } finally { this.busy = false; } }````FloatViewConfiguration.context` 必须是有效的 `UIAbilityContext`;从组件事件中可通过 `this.getUIContext().getHostContext()` 获取,再做类型确认。`pages/FocusFloatPage` 不是随意拼出的相对路径,必须登记在 `main_pages.json` 中;路径不合法可能得到 `1300016`。`ROUNDED_RECTANGLE` 是官方枚举,实际宽高不要在未查询限制时写死。这里还有一个容易忽略的失败分支:`create()` 已经创建了控制器,但 `setUIContext()` 失败,`controller` 并不会自动变回空值。示例为突出接口次序保留它供下一步错误处理;真实工程要设计明确的失败回收或整页重新初始化路径,不能立刻重复创建更多控制器。## 二、把系统回调当作唯一的显示确认### 1. 按钮连续点击不应产生两次 start用本地 `busy` 防抖只能防同步连点,不能覆盖 Promise 与系统事件间的间隔。下面的启动方法同时检查业务状态,回调则只负责吸收平台状态;这让网络计时、页面刷新和窗口启动不再共享一把含混的锁。arkts// 以下方法属于上文的 FocusFloatGate 类。private onFloatState = (info: floatView.FloatViewStateChangeInfo): void => { if (info.state === floatView.FloatViewState.STARTED) { this.setPhase(‘STARTED’); } else if (info.state === floatView.FloatViewState.STOPPED) { this.setPhase(‘STOPPED’); console.info(FocusRelay/FV-051 stopReason=${info.stopReason}); } else if (info.state === floatView.FloatViewState.ERROR) { this.setPhase(‘FAILED’); }};async startFromClick(): Promise { if (!this.controller || this.busy || (this.phase !== ‘PREPARING’ && this.phase !== ‘STOPPED’)) return; this.busy = true; this.setPhase(‘STARTING’); try { await this.controller.start(); console.info(‘FocusRelay/FV-051 start request resolved’); // 不在这里设置 STARTED:等待 onStateChange。 } catch (err) { this.setPhase(‘FAILED’); console.error(FocusRelay/FV-051 start failed ${JSON.stringify(err)}); } finally { this.busy = false; }}STARTED`、`STOPPED`、`ERROR` 是官方 `FloatViewState` 的真实取值;`STARTING` 则是应用在调用前设置的过渡态。`stopReason` 只有在 STOPPED 时有意义,不能在平时拿来推断退出原因。若启动遇到 `1300030`,优先核查是否重复操作;遇到 `1300033`,还应检查主窗口是否前台,以及是否已经存在其他闪控窗实例。为了让示例日志能手工对照,我给一次“正常请求”的模拟序列设定:`FV-051 phase=PREPARING`、`FV-051 phase=STARTING`、`start request resolved`、`FV-051 phase=STARTED`。最后一行只有收到回调时才能记录;文中的这组文字是**预期检查序列**,不是声称某台设备已经输出过它。### 2. 停止和移除监听要分开考虑停止不是把页面隐藏一下就算完成。用户可能在悬浮窗标题栏关闭,也可能把窗口拖进侧边栏;这些动作带来的状态与停窗原因应由回调决定。页面切换时也不能忘记取消监听,不然旧页面持有控制器,容易在新一轮启动时出现重复回调。```arkts// 同属 FocusFloatGate:只有状态确认是 STARTED 才允许用户主动停止。async stopFromClick(): Promise<void> { if (!this.controller || this.busy || this.phase !== 'STARTED') return; this.busy = true; this.setPhase('STOPPING'); try { await this.controller.stop(); console.info('FocusRelay/FV-051 stop request resolved'); // STOPPED 由 onStateChange 确认。 } catch (err) { this.setPhase('FAILED'); console.error(`FocusRelay/FV-051 stop failed ${JSON.stringify(err)}`); } finally { this.busy = false; }}releaseListener(): void { if (this.controller) { this.controller.offStateChange(this.onFloatState); }}offStateChange应传入注册时的同一个回调引用,不能临时写一个长得一样的新函数。监听解绑适合放在确定不会再接收当前控制器事件的销毁流程,不能在stop()Promise 一结束就抢先解绑,否则有可能漏掉最终 STOPPED。系统回调触发后再根据主页面生命周期释放,是更稳妥的时序。## 三、让主页面展示“可解释的等待”演示主页面只有三块信息:会话FV-051、专注时间25 分钟、窗口状态。用户第一次点击“开启闪控窗”以后,页面应显示STARTING与“等待系统回调”,而不是立刻显示“已开启”。只有收到 STARTED 才展示可结束按钮。图中的STARTED是为了说明最终状态的**合成演示界面**,并非真机运行证据。实际接入时,建议把窗口生命周期和计时器生命周期分离。停止闪控窗不等于结束专注任务;系统因主窗口销毁而停窗,也不应清除业务数据。FocusRelay 的计时数据由主页面业务层维护,闪控页只读取当前会话快照,避免窗口重建带来计时起点重置。此外,窗口权限不能仅凭配置文件里写了某个字符串便认定已授予。start()需要ohos.permission.FLOAT_VIEW,权限核验失败会涉及错误码201;示例未包含授权流程,必须依照目标设备、应用类型和最新官方权限指引实现授权及拒绝处理。没有授权时应允许用户正常留在主页面使用计时器。还有一个值得单独校验的交互:当窗口被系统折叠进侧边栏时,回调可能报告IN_SIDEBAR,但这不意味着任务终止;当应用主窗回到前台后,悬浮内容是否可见也不能只看主页面的@State。应根据FloatViewState的具体值更新「展示中/侧边栏/隐藏」提示,而不是把非 STARTED 状态一概归为失败。若用户主动关闭的是窗口而不是专注任务,重新打开主页面时仍应显示同一个FV-051,并让用户自行决定是否再创建闪控窗。在接口调测中,建议把失败路径变成可重复执行的测试夹具。比如连续点三次启动按钮,预期只有第一条通过本地门禁;第二、三次不应触发start()。再把主窗切到后台模拟不满足前台条件的情况,检查错误分支是否恢复按钮,而不是永久停在STARTING。这些都是可执行的测试设计,不是本文已取得的实测结论。## 四、调试与边界:把误判暴露在日志里调试先不追求视觉效果,只检查六类路径:设备不支持、首次正常启动、连续点击、主窗退到后台后启动、用户主动关闭、应用页面销毁。记录requestId`、业务状态、平台回调与错误码,但不要把用户个人信息或计时内容明文打印到 HiLog。尤其要看 Promise resolve 与 STARTED 的顺序,而不是只看有没有异常。另一个边界是闪控窗和普通全局悬浮窗不是同一个 API,不能照搬权限和生命周期。闪控窗属于 API 26 的能力,支持情况需在目标设备确认;模拟器展示图只是页面结构说明。把这个前提写清楚,比放一张看似成功的截图更有价值。最终判断标准也很简单:同一会话只创建并启动受控的一份控制器,按钮状态由平台回调校准,日志能够解释一次失败发生在哪一步。只有完成设备上的权限、生命周期、兼容性验证后,才应该把这套示例升级为可发布的闪控窗功能。官方核对资料: 闪控窗开发指导、floatView API 参考、闪控窗交互设计。参考文档截至 2026-10-19;请以项目实际 SDK 与目标设备提供的接口为准。
更多推荐
## 一、真正要判断的是哪一个完成时刻官方 



所有评论(0)