城市公交换乘页面最容易被忽略的并不是路径计算,而是同一条路线能否让不同阅读方式的乘客获得相同信息。此前的线路图已经把市民中心、科技馆与图书馆连接起来,候选路线保留了稳定键 L1-0835>L2-0848。当页面改成大字号,或者读屏服务按顺序读取卡片时,原先依赖颜色、图标和横向时间轴的表达就不够稳妥:蓝色线路与橙色线路需要有文字名称,换乘发生在哪个站、前后计划时刻以及是否存在通行条件变化,也必须以完整句子呈现。

公交路线读屏与大字号封面

从候选路径抽取可阅读的业务语义

页面中的路线信息不宜仅用“左侧蓝线、右侧橙线”来描述。第一篇建立的站点身份由线路与站点编号共同组成,第五篇又允许离线版本更新修改无台阶标记,因此阅读文本必须从当前有效的路线快照生成。以本地计划数据为例,市民中心 ST-A 在08:35登上L1青线,08:42到达科技馆 ST-B,08:48换乘L2橙线,09:05到达图书馆 ST-E。这些是静态计划时刻,不应被读成车辆当前位置。

读屏对象可以整理为五个连续语义单元:路线概要、出发站、换乘提示、到达站和操作按钮。概要先说明全程三十分钟与换乘一次;出发站读出线路与时刻;换乘提示说明在科技馆换乘,以及相隔六分钟的计划窗口;到达站读出目的地和时刻;操作按钮再明确可以查看站点详情。相比让读屏依次进入几个没有名称的箭头、颜色块和时钟图标,这样的顺序更接近乘客在现场做决定的过程。

业务语义应集中在一个纯函数中,避免屏幕版式不同而改变读出的句子。下面的Node.js逻辑将结构化路线数据投影为按顺序排列的阅读对象。它对必要字段做简单检查,保证阅读顺序由业务阶段决定,而不是由对象属性遍历顺序决定。

const assert = require('node:assert/strict');
const route = {
  key: 'L1-0835>L2-0848', origin: '市民中心', destination: '图书馆',
  lineA: 'L1青线', lineB: 'L2橙线', transfer: '科技馆',
  depart: '08:35', arriveTransfer: '08:42', nextDepart: '08:48', arrive: '09:05'
};
function speechUnits(r) {
  for (const field of ['key', 'origin', 'destination', 'transfer', 'depart', 'arrive']) {
    if (!r[field]) throw new Error('缺少字段:' + field);
  }
  return [
    { id: 'summary', text: `候选路线,${r.origin}到${r.destination},换乘一次` },
    { id: 'origin', text: `${r.depart}在${r.origin}乘坐${r.lineA}` },
    { id: 'transfer', text: `${r.arriveTransfer}到达${r.transfer},${r.nextDepart}换乘${r.lineB}` },
    { id: 'destination', text: `${r.arrive}到达${r.destination}` },
    { id: 'action', text: '查看完整站点详情' }
  ];
}
const units = speechUnits(route);
assert.deepEqual(units.map(x => x.id), ['summary','origin','transfer','destination','action']);
assert.match(units[2].text, /科技馆/);
console.log(units.map(x => x.text).join('\n'));

这段逻辑生成的阅读序列适合映射到ArkUI组件的无障碍文本属性,而不是让页面直接操作系统读屏服务。华为开发者的读屏适配资料说明,组件的 accessibilityText 可为视觉信息补充朗读内容,accessibilityGroup 可以将相关元素组织为一个阅读单元。组合范围需要谨慎:把整页都合成一个单元,会让使用者难以单独进入“查看详情”按钮;把时间数字拆散,又会导致08:35和08:42失去所属站点。卡片适合按完整语义分组,操作按钮则保留独立焦点。

大字号是重新排版,而不是放大一张画布

普通字号下,路线概要可以把“08:35→08:42→08:48→09:05”放在同一行。字号放大后,这样的排布会把站名挤到窄列里,中文换行的位置也会变得不可预测。更可靠的布局方式是让时间与站点形成垂直信息块,允许卡片高度随文本增长,减少固定像素高度,并把两个并列操作按钮在窄屏与大字号条件下改成上下排列。

布局断点与文字缩放属于两个不同维度。屏幕宽度较大,并不意味着文字一定较小;系统文字设置较大,也不应强行采用宽屏布局。页面可以根据当前可用宽度和实际字体比例决定每个卡片的展示模式,但不得依靠截断业务句子来换取视觉整齐。下方函数把可用宽度、字体比例与关键字段长度转为应用层布局建议;它不假设某种手机尺寸等于某个固定显示规格。

const assert = require('node:assert/strict');
function chooseCardLayout(widthVp, fontScale, labelLength) {
  if (![widthVp, fontScale, labelLength].every(Number.isFinite)) throw new TypeError('数值错误');
  if (widthVp <= 0 || fontScale <= 0 || labelLength < 0) throw new RangeError('参数范围错误');
  const contentWidth = Math.max(1, widthVp - 32);
  const estimatedTextWidth = labelLength * 16 * fontScale;
  const stacked = contentWidth < 420 || fontScale >= 1.4 || estimatedTextWidth > contentWidth * 0.65;
  return { mode: stacked ? 'stacked' : 'inline', allowWrap: true,
    buttonRows: stacked ? 2 : 1, minCardHeight: Math.ceil(88 * fontScale) };
}
assert.equal(chooseCardLayout(360, 1.6, 12).mode, 'stacked');
assert.equal(chooseCardLayout(850, 1, 6).mode, 'inline');
assert.equal(chooseCardLayout(850, 1.8, 6).buttonRows, 2);
console.log(chooseCardLayout(360, 1.6, 12));

这里的估算宽度用于选择安全的展示方案,不应当拿来替代真实排版引擎的测量结果。ArkUI中的Text和布局容器有自己的测量与换行过程,工程上仍需在多种系统字号、语言长度和屏幕宽度下检查是否有遮挡。尤其“无台阶优先”并非装饰标签:第五篇离线站点资料发生变更后,旧路线可能进入待复核状态,不能因为字体增大而把状态隐藏在折叠区域。文字放大也不应该把原本可点击的按钮挤出安全区域。

白色开发环境中的读屏语义与路线卡片

焦点应跟随稳定键,而非可见列表行号

路线筛选和字号切换往往会触发列表重新组织。如果页面只记住“第一个候选”或“第三行”,筛选无台阶路径后同一行可能已经对应另一条方案。上一章完成的版本合并还有另一种情况:原路线编号仍然存在,但因为科技馆无台阶状态变化,当前候选已经被标记为需要重新筛选。焦点恢复不能掩盖这个业务状态,更不能把存在问题的路线自动标成可出行。

应用可以保存 selectedKey 与 focusKey,并在新候选集合准备完成后解析目标。目标仍存在时恢复到它;目标被移除时回到路线列表标题,让用户重新选择。读屏顺序和视觉选中状态应分别管理:朗读焦点不一定就是导航选中项,误把两者强行同步,会在查看详情返回时造成焦点跳跃。可通过应用层状态转换函数明确处理筛选与字号变化。

const assert = require('node:assert/strict');
function resumeFocus(prev, candidates) {
  const ids = new Set(candidates.map(x => x.key));
  const selection = ids.has(prev.selectedKey) ? prev.selectedKey : null;
  const focus = ids.has(prev.focusKey) ? prev.focusKey : 'route-list-heading';
  const reviewNeeded = selection ? candidates.find(x => x.key === selection).reviewNeeded : false;
  return { selectedKey: selection, focusKey: focus, reviewNeeded: Boolean(reviewNeeded) };
}
const before = { selectedKey: 'L1-0835>L2-0848', focusKey: 'L1-0835>L2-0848' };
const revised = [{ key: 'L1-0835>L2-0848', reviewNeeded: true }];
assert.deepEqual(resumeFocus(before, revised), {
  selectedKey: 'L1-0835>L2-0848', focusKey: 'L1-0835>L2-0848', reviewNeeded: true
});
assert.equal(resumeFocus(before, []).focusKey, 'route-list-heading');
console.log(resumeFocus(before, revised));

页面阅读顺序与视觉排布分开核对

图中的页面把路线概要放在顶部、两段乘车步骤与换乘信息放在中部、操作按钮放在末尾。手机窄屏采用单列,长站名可以自动换行;宽屏可让候选列表在左、详情在右,但读屏顺序仍应遵守“当前候选→当前路线详情→可操作项”。不能让物理布局从左到右的遍历结果直接决定每一次朗读顺序,否则宽屏读到一半可能跳向另一条候选。

公交候选路线的大字号阅读界面

需要重点检查的异常包括:候选数据迟到时页面先聚焦空卡片;切换字号后焦点丢失;无障碍标签与可见文字引用了不同版本;按钮在放大状态下被固定底栏遮挡;读屏重复朗读同一段换乘提示;列表删除条目后焦点落到相邻的无关路线。每一种情况都应由稳定业务键、可追踪的状态变更和明确的回退位置处理,而不是凭借某个固定行号修补。

读屏的文字还需要区别计划时刻与实时状态。比如“08:48计划从科技馆发车”是清晰的描述,而“还有六分钟一定能接上”会把班次静态资料理解成现场承诺。对无障碍乘客而言,缺失坡道条件或站台通道信息应保持“待核对”标签,让路线排序和朗读表达遵守同一套有效性规则。

将可访问性检查纳入日常迭代

华为开发者读屏适配指南指出,阅读焦点应按逻辑次序遍历有效对象,并通过恰当的辅助文本传递对象含义。具体可参阅读屏适配指南与无障碍能力概览。前者适合核对 accessibilityText 与分组方式,后者帮助区分应用组件的语义属性和辅助服务本身的职责。实际工程中还应按目标SDK的ArkUI声明与设备类型分别确认可用属性。

每次修改路线卡片的数据字段、状态标签或布局,都应同步更新语义单元。检查应从路线概要能否独立理解开始,再检查出发站、换乘和终点,最后确认返回列表与查看详情的焦点位置。由此形成的好处不是让界面堆上更多辅助描述,而是让路线数据有一份既可供视觉布局、也可供朗读服务使用的单一业务表达。下一篇处理异常路线与恢复时,仍可复用这套语义顺序,不需要重新为不同设备编写一套站点解释逻辑。

Logo

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

更多推荐