闪控窗从完整态切到紧凑态视觉上通常只是少了几枚按钮、缩短了标题、把进度区域压成一行。对读屏服务来说事情并不只是“画面变小”如果旧节点还留在无障碍语义树里焦点可能继续落到不可见按钮如果每个子组件都单独播报用户要听完九个节点才能找到主操作如果形态切换和焦点恢复同时发生旧任务还会把焦点抢回已经消失的控件。本文用FloatA11y演示工程拆解这个问题。任务编号固定为A11Y-FLOAT-0060页面是CompactPanelPage完整态到紧凑态的目标不是“看起来没问题”而是把可访问节点从 9 个收敛为 5 个、隐藏焦点目标从 4 个降为 0并让恢复焦点稳定落到taskTitle。文中的结果是可复现演示数据不冒充真实项目测试结论。一、真正的异常发生在看不见的树里演示页最初有标题、进度、暂停、取消、展开、关闭、剩余时间、网络状态和任务编号九个可访问节点。进入紧凑态后界面只保留标题、进度、主操作、关闭和状态摘要五项。肉眼检查时一切正常但读屏焦点从进度继续右滑仍然会读到已经不显示的pauseButton与cancelButton。问题并不等同于“控件隐藏失败”。渲染树、命中树和无障碍语义树服务于不同目的。一个组件即使透明、移出可视区域或者被动画压到零尺寸也不意味着它一定不再被辅助服务识别。工程上需要把“当前是否可见”和“当前是否应该被读屏访问”明确建模而不是把语义行为寄托在视觉副作用上。这次演示把状态拆成三层FULL表示完整操作面板COMPACT表示紧凑控制条HIDDEN表示窗口已关闭。每次形态变化都生成新的semanticEpoch。只有与当前 epoch 一致的焦点恢复任务才允许执行旧任务直接丢弃。这样布局变化和无障碍焦点就有了同一套时序边界。基线数据如下完整态 9 个节点切换紧凑态后画面显示 5 项但语义树仍有 9 项其中 4 项不可见却可聚焦一次切换触发 3 条重复状态播报。目标数据则是 5 个节点、0 个隐藏焦点、1 条合并播报。这里的数字不是性能跑分而是审核语义树时可直接核对的验收项。二、先把视觉状态翻译成语义契约如果组件自己判断“我要不要被读”业务会很快出现分叉暂停按钮看isCompact取消按钮看expanded状态摘要又看任务进度。更稳妥的方式是由一个纯函数把窗口形态转换为语义契约UI 只消费契约。这段代码解决什么问题把窗口形态、主操作和可访问节点统一映射为不可变的语义快照避免各组件自行猜测。typePanelModeFULL|COMPACT|HIDDEN;interfaceSemanticSnapshot{epoch:number;mode:PanelMode;titleText:string;progressText:string;primaryActionText:string;hideSecondaryActions:boolean;targetFocusId:string;}functionbuildSemanticSnapshot(epoch:number,mode:PanelMode,progress:number):SemanticSnapshot{construnningprogress100;return{epoch,mode,titleText:文件同步任务 A11Y-FLOAT-0060,progressText:当前进度${progress}%,primaryActionText:running?暂停同步:查看结果,hideSecondaryActions:mode!FULL,targetFocusId:modeHIDDEN?:taskTitle};}这段写法的关键不是接口复杂而是它只接收事实并返回快照。COMPACT不再由五个组件分别判断hideSecondaryActions成为统一语义条件。状态从FULL进入COMPACT时epoch 从 14 增加到 15目标焦点同时变为taskTitle进入HIDDEN时目标为空不再请求焦点。容易出错的地方有两个。第一不要把progressText缓存在另一个对象中否则进度变化后读屏仍可能读旧值。第二不要把业务对象、窗口对象或 UIContext 塞进快照它应当保持可序列化、可记录、可测试。实际项目中可以为快照增加语言资源 key但不应直接拼接不可本地化的长文案。三、裁剪语义树而不是给隐藏节点“消音”ArkUI 的无障碍通用属性提供了显式语义控制。accessibilityText用于给节点提供稳定名称accessibilityDescription用于补充动作或后果accessibilityGroup可以把一组内容作为整体理解accessibilityLevel(no-hide-descendants)则可让当前组件及其子组件不被无障碍辅助服务识别。官方参考把该值的语义写得很明确因此它适合处理紧凑态下整组次要操作的退出。这段代码解决什么问题让次要操作在紧凑态真正退出无障碍语义树同时把标题和进度组合成稳定摘要。Componentstruct CompactPanelPage{Statesnapshot:SemanticSnapshotbuildSemanticSnapshot(14,FULL,68);build(){Column({space:12}){Column({space:4}){Text(文件同步).id(taskTitle)Text(68% · 还需约 2 分钟)}.accessibilityGroup(true).accessibilityText(this.snapshot.titleText).accessibilityDescription(this.snapshot.progressText)Row({space:8}){Button(暂停).accessibilityText(暂停同步)Button(取消).accessibilityText(取消同步任务)Button(展开).accessibilityText(展开完整控制面板)Button(网络).accessibilityText(查看网络状态)}.accessibilityLevel(this.snapshot.hideSecondaryActions?no-hide-descendants:auto)Button(this.snapshot.primaryActionText).accessibilityDescription(执行当前任务的主要操作)Button(关闭).accessibilityText(关闭闪控窗)}}}这里没有把每个隐藏按钮分别设置为不可访问而是对“次要操作组”一次性裁剪。原因是紧凑态的产品语义本来就是整组退出逐个设置很容易漏掉后续新增按钮。accessibilityGroup(true)也不是为了减少组件数量而滥用它只包裹标题与进度这组天然相关的信息让用户一次听到任务名和当前状态。状态变化可以这样理解渲染层仍可保留动画所需的容器但语义层立刻从 9 节点收敛到 5 节点。最容易犯的错误是把整个根容器设置为no-hide-descendants这样关闭按钮也会消失另一个错误是在分组后仍给每个子节点设置冗长描述造成同一信息重复播报。实际项目要用读屏遍历顺序检查而不是只看属性是否写上。图中的 DevEco Studio 画面是演示配图不是实际 IDE 测试证据。左侧项目结构把SemanticSnapshot.ets、CompactPanelPage.ets与FocusCoordinator.ets分开中间标出no-hide-descendants右侧模拟器显示 68% 紧凑态底部 HiLog 对应epoch15 nodes5 hidden0 focustaskTitle。四、焦点恢复必须服从形态事务语义树收敛后第二个问题才出现从完整态切到紧凑态时原焦点可能位于即将退出语义树的按钮。此时需要把焦点迁移到稳定锚点但不能在状态赋值后立刻无条件调用requestFocus。组件树提交需要时间而且用户可能在这段间隙再次关闭窗口。演示采用 generation/epoch 模式每次形态改变先递增 epoch再提交新快照焦点任务记录自己的 epoch下一帧执行时重新比对。只有窗口仍可见、目标仍存在、任务仍是最新一代时才请求焦点。这段代码解决什么问题阻止旧的异步焦点任务把读屏焦点抢回已退出语义树的控件。classFocusCoordinator{privatecurrentEpoch:number0;beginTransition():number{this.currentEpoch1;returnthis.currentEpoch;}restoreAfterCommit(epoch:number,targetId:string):void{if(targetId.length0){return;}setTimeout((){if(epoch!this.currentEpoch){console.info([FloatA11y] stale focus dropped epoch${epoch});return;}constacceptedfocusControl.requestFocus(targetId);console.info([FloatA11y] focus target${targetId}accepted${accepted}epoch${epoch});},0);}}constfocusCoordinatornewFocusCoordinator();functionswitchToCompact(progress:number):SemanticSnapshot{constepochfocusCoordinator.beginTransition();constnextbuildSemanticSnapshot(epoch,COMPACT,progress);focusCoordinator.restoreAfterCommit(epoch,next.targetFocusId);returnnext;}这里的setTimeout(..., 0)只代表“把请求放到当前同步状态修改之后”并不是对组件提交时长的硬编码保证。更复杂页面应使用自身已有的布局完成信号或生命周期边界但仍要保留 epoch 校验。状态从 epoch 14 切到 15 后如果紧接着关闭窗口进入 epoch 16epoch 15 的任务会记录stale focus dropped不会执行请求。容易出错的是只检查targetId不检查窗口当前形态。ID 字符串可能被另一个页面复用请求成功也不代表焦点落在业务预期对象上。工程中应给锚点使用页面内稳定、唯一的 ID并在页面销毁或窗口关闭时递增 epoch。焦点恢复不是一次性的 UI 技巧而是与窗口生命周期成对管理的资源。五、把重复播报收成一条状态摘要紧凑态切换常伴随三个变化窗口形态改变、操作集合改变、进度文本刷新。如果每个变化都触发自己的播报用户会连续听到“窗口已收起”“按钮已隐藏”“当前进度 68%”信息量不大打断感却很强。本例不在文章中虚构系统播报接口而是在应用层先实现一条可测试的摘要合并器。它只负责决定“本次事务应该说什么”具体如何触发辅助服务事件应按项目目标版本的官方 API 和测试方案接入。这样可以避免为了展示而写出不存在或版本不匹配的接口。这段代码解决什么问题把同一形态事务中的多条变化压成一条可审计摘要并过滤过期事务。interfaceSemanticAnnouncement{epoch:number;text:string;}functionbuildAnnouncement(snapshot:SemanticSnapshot,visibleNodeCount:number):SemanticAnnouncement|undefined{if(snapshot.modeHIDDEN){return{epoch:snapshot.epoch,text:文件同步控制窗已关闭};}if(snapshot.modeCOMPACT){return{epoch:snapshot.epoch,text:控制窗已收起${snapshot.progressText}${visibleNodeCount}个可访问项目};}returnundefined;}constannouncementbuildAnnouncement(buildSemanticSnapshot(15,COMPACT,68),5);// 控制窗已收起当前进度 68%5 个可访问项目为什么只返回文本而不直接发事件因为语义决策和平台调用的失败模型不同。纯函数可以做单元测试、快照对比和多语言检查平台事件需要考虑服务是否启用、页面是否仍存活、是否与系统自动播报重复。把两者耦合后测试往往只能靠耳朵听很难进入持续集成。实际项目还要注意隐私摘要中不要朗读文件名、联系人或支付信息等敏感内容。标题也不应只依赖图标含义。本文的文件同步任务 A11Y-FLOAT-0060是演示标识生产中可在锁屏、投屏或隐私模式下退化为“后台任务”。六、运行页先看焦点路径再看视觉细节运行页固定时间为 06:18状态栏显示 Wi‑Fi、5G、信号与 66% 电量。页面展示FloatA11y / CompactPanelPage任务 ID 为A11Y-FLOAT-0060进度 68%模式COMPACT。红色箭头从“当前焦点 taskTitle”指向标题与进度摘要旁边标注9 → 5 nodes和hidden focus 4 → 0。这张图承担的是“可操作结果”说明紧凑态仍有清晰主操作焦点从标题开始随后经过主操作和关闭不再进入隐藏按钮。验证时不要只按一次右滑应从窗口创建、完整态进入紧凑态、紧凑态回完整态、关闭窗口四条路径分别遍历。任何路径出现幽灵节点都说明语义状态没有与视觉状态一起提交。七、诊断页要能回答是哪一代状态出了问题详情页时间同样是 06:18显示semanticEpoch 15、FULL → COMPACT、visibleNodes 5、hiddenFocusable 0、announcement 1和focus taskTitle。日志区保留一条epoch14 stale focus dropped用红圈强调旧请求已被拒绝。与运行页相比它不展示漂亮的控制条而是展示状态事务的证据链。诊断字段要少而稳定。建议至少保留模式、epoch、目标焦点、可访问节点数、隐藏可聚焦节点数和摘要次数。不要把整棵无障碍树长期写入生产日志节点文本可能包含隐私且大对象会放大日志开销。开发构建可以导出脱敏快照正式构建只保留计数和匿名 ID。演示验收结果为节点 9→5隐藏焦点 4→0重复播报 3→1旧 epoch 14 的焦点任务被丢弃epoch 15 的taskTitle请求被接受。这里的“接受”指应用调用返回值与演示状态一致不等价于所有辅助服务、语言和设备形态都已通过真实用户测试。八、边界比结论更重要第一accessibilityLevel(no-hide-descendants)适合整组退出语义树不适合用来遮掩本应可操作但标签写得不好的控件。可见且可点击的关键操作应补足名称、状态和后果而不是简单排除。第二accessibilityGroup(true)会改变遍历粒度。标题与进度适合组合多个独立按钮通常不适合组合。分组过度会让用户失去逐项操作能力分组不足则会造成播报碎片化。第三焦点恢复一定要和窗口生命周期成对。窗口关闭、页面销毁、Ability 切后台或模式再次变化时都应让旧任务失效。仅靠延时数字无法建立正确性。第四本文只覆盖应用自己的 ArkUI 语义树。若闪控窗中嵌入 XComponent、自绘内容或三方渲染引擎需要按照对应无障碍接入方式提供节点、Action 和事件不能期待 ArkUI 自动理解像素内容。最后建议把无障碍验收写成数据visibleNodes5、hiddenFocusable0、announcement1、focustaskTitle。这些字段比“读起来还行”更适合作为回归基线也能让视觉、交互和无障碍三条链路在同一次形态事务中对齐。九、测试矩阵不能只覆盖一次收起动作如果测试脚本只有“打开窗口—点击收起—向右滑动”很容易漏掉真正麻烦的路径。闪控窗往往由任务状态、窗口状态和系统状态共同驱动。任务在 68% 时进入紧凑态与任务恰好完成时进入紧凑态主操作文案不同窗口位于前台与 Ability 切到后台再恢复焦点锚点的可用性也不同横竖屏或字体放大后标题可能换行分组摘要仍应保持一次播报。演示工程把回归矩阵分成四个维度。第一维是形态FULL、COMPACT、HIDDEN。第二维是任务RUNNING、PAUSED、COMPLETED、FAILED。第三维是进入方式用户点击、系统窗口调整、任务回调触发。第四维是辅助设置读屏关闭、读屏开启、字体放大。不是所有组合都需要人工逐项执行但必须挑出状态边界RUNNING/FULL 到 RUNNING/COMPACTPAUSED/COMPACT 到 COMPLETED/COMPACT以及 COMPACT 在后台恢复后回到 FULL。每条用例至少检查五件事。其一视觉节点与语义节点是否对应其二隐藏节点能否被辅助服务发现其三焦点锚点是否仍在当前页面其四摘要是否重复其五关闭后是否还有延迟任务触发。这样定义后“能听到”不再等于通过“焦点可达、顺序合理、状态准确、退出干净”才是完整结果。对自动化测试来说可以先验证纯函数同一输入必须生成同一SemanticSnapshotFULL 的hideSecondaryActions为 falseCOMPACT/HIDDEN 为 trueHIDDEN 的目标焦点为空。再验证状态协调器连续生成 epoch 14、15、16 时只有 16 的任务有资格执行。最后才在设备上检查真实辅助服务行为。分层的价值在于平台测试失败时能先排除业务状态错误。还要专门验证重复进入。很多窗口组件在快速点击时会收到两次收起意图如果两次都递增 epoch 并生成同样快照虽然最终界面正确却可能产生两次播报。解决办法不是简单加一个固定防抖时间而是在提交前比较目标模式当前已经是 COMPACT 时重复意图应返回同一状态不创建新的语义事务。防抖处理时间幂等处理事实两者不能混为一谈。十、日志、隐私和发布检查要同时收口开发阶段为了定位焦点问题团队往往会把节点文本、完整树结构和事件序列全部写入 HiLog。短期很好用长期却可能留下隐私风险。任务标题、文件名和通知内容都可能出现在无障碍文本里。正式版本建议只记录匿名节点 ID、模式、epoch、计数和结果码需要导出详细树时使用明确的调试开关并在导出前脱敏。日志格式也要保持可关联。本文使用固定前缀[FloatA11y]一次事务至少带taskIdA11Y-FLOAT-0060与epoch15。窗口层记录FULL→COMPACT语义层记录nodes5 hidden0焦点层记录targettaskTitle acceptedtrue。三条日志靠同一个 epoch 串联比按时间猜测哪条回调属于哪次切换可靠得多。发布前不应把“读屏测试”留给最后一天。组件新增按钮时代码评审就要回答它在 FULL、COMPACT、HIDDEN 三种状态下的无障碍级别修改标题或进度组合时要检查是否造成重复播报更改关闭逻辑时要确认 epoch 是否失效。把这些问题放进评审模板能减少上线前集中修补。真实用户测试仍然不可替代。自动化可以发现隐藏节点和顺序变化却无法完整判断文案是否自然、信息是否过载、用户是否能形成空间预期。演示中的“控制窗已收起当前进度 68%5 个可访问项目”适合调试但正式文案可能不需要朗读项目数量。数量是工程证据不一定是用户价值。产品文案应在不丢失状态的前提下尽量短。最后再做一次成对检查创建窗口与销毁窗口成对注册监听与注销监听成对进入语义事务与 epoch 失效成对初始化焦点锚点与页面退出成对。闪控窗的问题常被误解为尺寸适配实际上它更像一个短生命周期的状态容器。只有视觉、输入、语义和生命周期同时提交紧凑态才是真正收敛而不是把复杂度藏到屏幕之外。本次演示的最终判断并不是“增加几行无障碍属性就结束”而是把语义树纳入窗口状态机。FULL、COMPACT、HIDDEN 各自拥有可核对的节点集合形态变化拥有 epoch焦点恢复只能消费最新快照播报文本先经过合并再交给平台层。这样设计后按钮增减、标题改版或任务状态扩展都能沿着同一条审计链检查不必再靠测试人员偶然发现幽灵焦点。如果团队只能先做一件事建议先给紧凑态列出“应该被读到的五个节点”再逐项对照实现。明确的允许清单通常比不断修补排除规则更可靠。等节点集合稳定后再增加焦点事务和诊断字段问题会从难以描述的体验反馈转化为可复现、可回归的工程差异。官方资料核对日期2026-10-01闪控窗开发指南ArkUI 无障碍属性参考Accessibility Kit 概述