把阅读工作区从手机拿到折叠屏,列表和正文都能显示,似乎就算完成了适配。可当用户开启屏幕朗读、接入键盘,再把窗口缩窄时,问题才真正暴露出来:高亮停在一篇文章上,朗读却只说“按钮”;焦点从左栏跳到右栏以后返回方向不明确;界面重建成功,却把原来读到的文章位置重置。视觉上正确的分栏,并不能自动保证阅读体验正确。

这一篇以同一个阅读工作区收尾。前五篇已经约定 ReadingItem.id 是身份、selectedId 是唯一选中源、ReadingMark 用于保存锚点,窄窗借助导航栈呈现正文,宽窗则在同一窗口内显示列表与详情。我们不再开发另一个平台专用页面,而是将布局、状态和可访问性放到同一个验收矩阵里,逐项寻找设计的薄弱点。

多端阅读与无障碍验收封面

一、早上九点:先把“看得见”改写成能验收的条件

评估入口分为窗口形态和操作方式两个维度。窗口形态至少覆盖窄窗单栏、中等宽度双栏、宽窗双栏,以及宽窄往返切换;操作方式覆盖触控、屏幕朗读与外接键盘。两项交叉,才能找到仅在某种组合下出现的问题。不能把“手机上触屏点击正常”当作平板键盘可用的证据,也不能用一个固定横屏截图说明动态窗口尺寸适配已经完成。

对每个组合只追四个可观察结果。第一是当前选中的文章身份是否正确;第二是进入详情后是否具有能表达文章内容的朗读文本;第三是键盘操作能否抵达关键按钮并按合理次序移动;第四是从宽窗切回窄窗时,保存与恢复过程是否遵守既有阅读锚点规则。验收记录应写入操作路径、预期结果、实际发现与复现条件,而不是简单勾选“支持多设备”。

这里还需要区分窗口宽度和硬件类别。折叠态、平板分屏、自由窗口都会改变实际可用宽度,同一设备也可以出现不同分栏。我们继续依靠 GridRow 和 GridCol 的断点列数表达布局,不依据某个宣传中的设备功能推测存在可调用的专有折叠 API。布局决定区域如何排布,业务状态决定展示哪篇文章,这两件事情仍然必须分开。

断点布局与阅读状态的验收关系

二、午后排查:高亮不能代替语义

视觉版列表使用背景色表示被选中,但对屏幕朗读用户来说,颜色本身不是信息。单靠 Text 显示标题,也不一定说明按下这个条目会发生什么动作。更清晰的做法是把真正可操作的区域写成按钮,并为它提供描述当前行为和选中状态的语义。华为 ArkUI 的 accessibilityText 可指定读屏文本,accessibilityDescription 则可补充操作用途;装饰性元素可以通过 accessibilityLevel('no') 避免占据无意义的读屏节点。不能为了简化焦点,把整片包含多个可操作按钮的区域错误合并成不可细分的朗读组。

下面的独立验收页沿用系列中的文章身份模型,以三个可访问的阅读条目说明最小实现。它不重新实现前几篇的 Preferences 和 NavPathStack;正式工程把选中回调接入已有协调器即可。GridRow 的栅格在窄窗口会自然排列为上下区域,双栏按断点分配空间;页面状态由一个 selectedId 控制。

class ReadingItem {
  readonly id: string;
  readonly title: string;
  readonly summary: string;
  constructor(id: string, title: string, summary: string) {
    this.id = id;
    this.title = title;
    this.summary = summary;
  }
}

@Entry
@Component
struct ReaderAccessibilityAudit {
  @State selectedId: string = 'reader_intro';
  private items: ReadingItem[] = [
    new ReadingItem('reader_intro', '分栏阅读入门', '认识列表与详情的分工'),
    new ReadingItem('reader_mark', '阅读锚点管理', '保存并恢复文章位置'),
    new ReadingItem('reader_perf', '长列表优化', '保持稳定身份与可见区更新')
  ];

  private current(): ReadingItem {
    const found: ReadingItem | undefined =
      this.items.find((item: ReadingItem) => item.id === this.selectedId);
    return found === undefined ? this.items[0] : found;
  }

  build() {
    Column({ space: 12 }) {
      Text('阅读工作区').fontSize(26).fontWeight(FontWeight.Bold)
      GridRow({ columns: { sm: 4, md: 8, lg: 12 }, gutter: 12 }) {
        GridCol({ span: { sm: 4, md: 3, lg: 4 } }) {
          Column({ space: 8 }) {
            Text('文章目录').fontSize(20)
            ForEach(this.items, (item: ReadingItem) => {
              Button(item.title, { type: ButtonType.Normal })
                .width('100%')
                .height(52)
                .backgroundColor(this.selectedId === item.id ?
                  Color.Blue : 0x46536B)
                .accessibilityText(`${item.title},${this.selectedId === item.id ? '已选中' : '未选中'}`)
                .accessibilityDescription('激活以阅读文章')
                .onClick(() => { this.selectedId = item.id; })
            }, (item: ReadingItem) => item.id)
          }
          .width('100%')
        }
        GridCol({ span: { sm: 4, md: 5, lg: 8 } }) {
          Column({ space: 12 }) {
            Text(this.current().title).fontSize(22)
            Text(this.current().summary).fontSize(18)
            Button('返回目录')
              .accessibilityDescription('返回阅读列表并保留当前文章')
              .onClick(() => { this.selectedId = 'reader_intro'; })
          }
          .width('100%')
        }
      }
      .width('100%')
    }
    .padding(18)
    .width('100%')
  }
}

这段代码用于验证语义和状态流,而不是替换真实业务导航。真实窄窗可能由 Navigation 展开详情页;因而“返回目录”应调用既有页面栈行为,而不是把 selectedId 强制设回首项。示例里的重置按钮只是页面内的独立操作,接入工程时要替换为协调器的导航方法。对于长列表页,前一篇的 Repeat.virtualScroll() 属于 V2 状态管理路径,不应与这里的 V1 @State 写法直接拼接,迁移时应统一状态模型或做明确的组件边界适配。

按钮高度和颜色只是验收界面的可调整参数,不能以固定的 52vp 声称已满足所有交互目标。真实场景需要结合目标系统的触控、焦点指示和大字体设置观察,文本变长时也不能让行裁掉关键信息。尤其是本地化标题,中文短标题和英文长标题会产生完全不同的换行结果。

阅读条目的朗读语义与焦点拆解

三、傍晚联调:把跨窗状态和读屏焦点分开追踪

用户在平板双栏选中“阅读锚点管理”,然后缩小窗口,左侧目录与详情的呈现方式会变化,但 selectedId 必须仍指向同一 id。读屏焦点却不是 selectedId:它由系统可访问节点和当时的交互上下文决定。贸然把当前索引数同时用作滚动锚点、文章身份和无障碍焦点目标,列表一旦排序或节点复用,就可能错位。需要保留三套不同的概念:业务文章身份、阅读内容锚点、当前交互焦点。它们可以协同,但不能相互替代。

如果返回后焦点落在页面外面,先确认对应按钮是否仍然存在,再检查切换过程中 ForEach 的 key 是否稳定,最后检查导航堆栈和页面生命周期是否发生了预期外的重建。如果读屏始终报“按钮”,回头确认 .accessibilityText() 是否设置在真正可聚焦的组件上,而不是设置在没有参与点击事件的装饰容器上。若详细页标题被重复朗读,则检查是否同时为外层容器和内层文字配置了重复语义,避免人为制造两次相同播报。

这一阶段建议把执行记录分成“触控操作轨迹”“读屏播报内容”“键盘焦点次序”三栏,逐个记录,不把无法复现的偶发失焦直接归类为布局错误。特别要保留窗口尺寸变化前后的文章 id、目录顺序和入口来源。只留下截图而不记录操作顺序,常常无法区分是重新选择、导航返回还是系统恢复引发的差异。

四、收工前的验收清单不应该只有绿色勾选

最终要做三组回归:从窄窗打开文章、回到列表、再打开另一篇;在宽窗持续切换三篇文章,并确认高亮、标题和正文一致;保持一篇文章的阅读锚点,改变窗口宽度和应用前后台状态,再确认恢复目标没有因列表索引改变而漂移。每组都应分别在常规字体、系统放大字体、读屏开启以及外接键盘条件下执行。对于测试环境不存在的组合,清单应标记待验证,不要假设其它设备通过即可等价通过。

多设备与无障碍回归验收路线

面对失败要能定位原因。布局错位回到栅格断点与容器约束;选中错乱回到唯一状态源和稳定 id;朗读不足回到可访问文本与可操作组件;焦点跳转异常回到组件树变化和导航时序;阅读位置丢失则回到 ReadingMark、快照持久化和恢复顺序。这样每类问题都有明确负责人和复测入口,不会被“多端适配问题”这个宽泛标签盖住。

六篇文章贯穿的一条规则是:设备形态允许变化,但用户操作的语义不应随意变化。GridRow 解决空间,NavPathStack 解决路径,selectedId 解决身份,ReadingMark 解决位置,accessibilityText 解决可感知的描述。无障碍验收不是在项目结束时额外补一次“读屏支持”,而是验证这些分工真正一致的最后一关。

官方参考:

Logo

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

更多推荐