主题
无障碍设计规范
最后更新:2026-02-21 版本:v1.0 状态:✅ 强制执行
本文档定义 Reading Vocab Helper 项目的 无障碍(Accessibility)设计规范,包括触摸目标、颜色对比度、语义化标签、屏幕阅读器兼容等。
📋 目录
触摸目标最小尺寸
核心原则:所有可点击元素的触摸目标 ≥ 48x48 dp
WCAG 和 Material 3 要求
| 标准 | 最小尺寸 | 推荐尺寸 |
|---|---|---|
| WCAG 2.1 Level AA | 44x44 px | 48x48 px |
| Material 3 | 48x48 dp | 48x48 dp |
| iOS HIG | 44x44 pt | 44x44 pt |
Flutter 默认行为
✅ 自动符合的组件:
| 组件 | 触摸区域 | 说明 |
|---|---|---|
IconButton | 48x48 dp | 默认符合 |
ElevatedButton | 高度 40dp,宽度自适应 | 需检查宽度 |
FilledButton | 高度 40dp,宽度自适应 | 需检查宽度 |
TextButton | 高度 40dp,宽度自适应 | 需检查宽度 |
Checkbox | 48x48 dp(包含 padding) | 默认符合 |
Radio | 48x48 dp(包含 padding) | 默认符合 |
Switch | 48x48 dp(包含 padding) | 默认符合 |
ListTile | 高度 ≥ 48dp | 默认符合 |
⚠️ 需要注意的组件:
| 组件 | 默认行为 | 建议 |
|---|---|---|
GestureDetector | 无默认尺寸 | 手动确保 ≥ 48x48 |
InkWell | 无默认尺寸 | 手动确保 ≥ 48x48 |
| 自定义按钮 | 无默认尺寸 | 手动确保 ≥ 48x48 |
| 小图标按钮 | 可能 < 48 | 增加 padding |
代码示例
dart
// ❌ 错误:触摸目标过小
GestureDetector(
onTap: onTap,
child: Container(
width: 24, // 太小!
height: 24,
child: Icon(Icons.close, size: 16),
),
)
// ✅ 正确:使用 IconButton
IconButton(
onPressed: onTap,
icon: Icon(Icons.close),
iconSize: 24,
// 默认触摸区域 48x48
)
// ✅ 正确:手动确保尺寸
GestureDetector(
onTap: onTap,
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: Icon(Icons.close, size: 24),
),
)
// ✅ 正确:使用 SizedBox 包裹
SizedBox(
width: 48,
height: 48,
child: InkWell(
onTap: onTap,
customBorder: CircleBorder(),
child: Icon(Icons.favorite, size: 24),
),
)
// ✅ 正确:使用 IconButton.filledTonal
IconButton.filledTonal(
onPressed: onTap,
icon: Icon(Icons.edit),
// 自动 48x48 触摸区域
)密集布局例外
在某些密集布局场景(如数据表格),可以使用较小的触摸目标,但需要:
- 在文档中标注例外原因
- 提供替代访问方式(如键盘导航)
- 确保间距足够避免误触
dart
// 密集布局例外(需添加注释)
// ACCESSIBILITY_EXCEPTION: 数据表格密集布局,提供键盘导航
DataTable(
columnSpacing: 12,
rows: rows.map((row) => DataRow(
cells: [
DataCell(
SizedBox(
width: 36, // 例外:表格单元格
height: 36,
child: IconButton(
iconSize: 16,
onPressed: () {},
icon: Icon(Icons.edit),
),
),
),
],
)).toList(),
)颜色对比度
核心原则:文字与背景的对比度 ≥ 4.5:1
WCAG 对比度要求
| 级别 | 普通文字 | 大文字(≥18sp 或 14sp 粗体) |
|---|---|---|
| AA | ≥ 4.5:1 | ≥ 3:1 |
| AAA | ≥ 7:1 | ≥ 4.5:1 |
Material 3 颜色对比度
Material 3 ColorScheme 已经设计为符合对比度要求:
| 前景色 | 背景色 | 对比度 |
|---|---|---|
onPrimary | primary | ≥ 4.5:1 ✅ |
onSecondary | secondary | ≥ 4.5:1 ✅ |
onSurface | surface | ≥ 4.5:1 ✅ |
onSurfaceVariant | surface | ≥ 4.5:1 ✅ |
onError | error | ≥ 4.5:1 ✅ |
危险组合
❌ 避免以下组合:
| 前景色 | 背景色 | 对比度 | 问题 |
|---|---|---|---|
tertiary | surface | 可能 < 4.5 | 使用 onSurface |
outline | surface | 可能 < 4.5 | 仅用于边框 |
| 浅灰色 | 白色背景 | < 3:1 | 对比度不足 |
代码示例
dart
// ❌ 错误:对比度可能不足
Text(
'重要文字',
style: TextStyle(
color: colorScheme.tertiary, // 可能对比度不足
),
)
// ✅ 正确:使用语义化颜色
Text(
'重要文字',
style: TextStyle(
color: colorScheme.onSurface, // 确保对比度
),
)
// ✅ 正确:强调文字使用 primary
Text(
'强调文字',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600, // 加粗可降低对比度要求
),
)
// 检查对比度工具函数
double calculateContrastRatio(Color foreground, Color background) {
final l1 = foreground.computeLuminance();
final l2 = background.computeLuminance();
final lighter = math.max(l1, l2);
final darker = math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}禁用状态
禁用状态的文字对比度可以降低,但不应完全不可见:
dart
// 禁用状态
Text(
'禁用文字',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.38), // Material 3 标准
),
)语义化标签
核心原则:为图标和图像提供 semanticsLabel
什么是 semanticsLabel
semanticsLabel 是屏幕阅读器朗读的文字描述,帮助视障用户理解 UI 元素的含义。
需要 semanticsLabel 的组件
| 组件 | 必要性 | 说明 |
|---|---|---|
Icon | 推荐 | 除非是装饰性图标 |
Image | 必要 | 所有内容图像 |
IconButton | 推荐 | 或使用 tooltip |
| 自定义图形 | 必要 | 使用 Semantics 包裹 |
代码示例
dart
// ✅ 正确:Icon 添加语义标签
Icon(
Icons.favorite,
semanticsLabel: '收藏',
)
// ✅ 正确:Image 添加语义标签
Image.asset(
'assets/images/logo.png',
semanticsLabel: 'Reading Vocab Helper 应用 Logo',
)
// ✅ 正确:IconButton 使用 tooltip
IconButton(
icon: Icon(Icons.delete),
tooltip: '删除', // 同时提供视觉提示和语义
onPressed: onDelete,
)
// ✅ 正确:自定义图形使用 Semantics
Semantics(
label: '学习进度:75%',
child: CustomProgressPainter(progress: 0.75),
)
// ✅ 正确:排除装饰性图标
Icon(
Icons.arrow_right,
semanticsLabel: null, // 明确排除
)
// 或使用 ExcludeSemantics
ExcludeSemantics(
child: Icon(Icons.arrow_right),
)装饰性元素
装饰性元素不需要 semanticsLabel:
- 纯装饰性图标(箭头指示、分隔符)
- 已有文字说明的图标(如带标签的按钮)
- 背景图案
dart
// 装饰性箭头,使用 ExcludeSemantics
Row(
children: [
Text('下一步'),
ExcludeSemantics(
child: Icon(Icons.arrow_forward), // 装饰性
),
],
)
// 有文字说明的按钮,图标不需要额外语义
FilledButton.icon(
onPressed: onPressed,
icon: Icon(Icons.save),
label: Text('保存'), // 按钮整体已有语义
)屏幕阅读器兼容
核心原则:确保所有内容可被屏幕阅读器正确朗读
Flutter Semantics Tree
Flutter 自动构建 Semantics Tree,但需要确保:
- 所有交互元素有明确的语义
- 阅读顺序符合视觉逻辑
- 动态内容变化被正确通知
常用 Semantics 属性
| 属性 | 用途 | 示例 |
|---|---|---|
label | 元素描述 | "搜索按钮" |
value | 当前值 | "音量 50%" |
hint | 操作提示 | "双击激活" |
button | 标记为按钮 | button: true |
header | 标记为标题 | header: true |
sortKey | 自定义阅读顺序 | OrdinalSortKey(1.0) |
代码示例
dart
// 自定义语义
Semantics(
label: '收藏按钮',
hint: '双击添加到收藏',
button: true,
child: GestureDetector(
onTap: onTap,
child: CustomHeartIcon(),
),
)
// 标题语义
Semantics(
header: true,
child: Text(
'词汇列表',
style: context.headlineMedium,
),
)
// 动态内容通知
SemanticsService.announce(
'已添加到收藏',
TextDirection.ltr,
);
// 自定义阅读顺序
Stack(
children: [
Semantics(
sortKey: OrdinalSortKey(2.0),
child: BackgroundImage(),
),
Semantics(
sortKey: OrdinalSortKey(1.0), // 先读取
child: ContentText(),
),
],
)测试屏幕阅读器
iOS:
- 设置 → 辅助功能 → VoiceOver → 开启
- 在应用中滑动浏览,检查朗读内容
Android:
- 设置 → 无障碍 → TalkBack → 开启
- 在应用中滑动浏览,检查朗读内容
焦点管理
核心原则:支持键盘导航和焦点控制
焦点顺序
焦点顺序应遵循视觉阅读顺序(通常从左到右,从上到下)。
dart
// 自定义焦点顺序
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: TextField(controller: _nameController),
),
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: TextField(controller: _emailController),
),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: FilledButton(
onPressed: onSubmit,
child: Text('提交'),
),
),
],
),
)焦点指示器
确保焦点状态明显可见:
dart
// 自定义焦点样式
Focus(
onFocusChange: (hasFocus) {
setState(() => _focused = hasFocus);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: _focused
? colorScheme.primary
: Colors.transparent,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
child: content,
),
)焦点陷阱
对话框等模态应捕获焦点:
dart
// 对话框自动处理焦点陷阱
showDialog(
context: context,
builder: (context) => AlertDialog(
// Flutter 自动处理焦点陷阱
title: Text('确认'),
actions: [
TextButton(onPressed: () {}, child: Text('取消')),
FilledButton(
autofocus: true, // 自动获取焦点
onPressed: () {},
child: Text('确认'),
),
],
),
)检查清单
开发时检查
- [ ] 所有可点击元素触摸区域 ≥ 48x48 dp
- [ ] 文字与背景对比度 ≥ 4.5:1
- [ ] 所有图标有
semanticsLabel(除装饰性) - [ ] 所有图像有
semanticsLabel - [ ] IconButton 有
tooltip - [ ] 标题使用
header语义 - [ ] 焦点顺序符合视觉逻辑
- [ ] 焦点状态明显可见
测试时检查
- [ ] 使用 VoiceOver (iOS) 测试
- [ ] 使用 TalkBack (Android) 测试
- [ ] 使用 Flutter Inspector 检查 Semantics Tree
- [ ] 使用键盘完整导航应用
- [ ] 检查暗色模式下的可读性
代码审查检查
bash
# 检查缺少 semanticsLabel 的 Icon
grep -rn "Icon(" lib/ | grep -v "semanticsLabel\|// no-semantics"
# 检查小尺寸触摸目标
grep -rn "GestureDetector\|InkWell" lib/ | \
grep -A5 "width:\s*[0-3][0-9]\|height:\s*[0-3][0-9]"
# 检查硬编码颜色(可能对比度不足)
grep -rn "Colors\.\|Color(0x" lib/快速参考
触摸目标
| 组件 | 最小尺寸 | Flutter 默认 |
|---|---|---|
| IconButton | 48x48 dp | ✅ 符合 |
| Button | 高度 40dp | ⚠️ 检查宽度 |
| Checkbox | 48x48 dp | ✅ 符合 |
| ListTile | 高度 ≥ 48dp | ✅ 符合 |
| 自定义按钮 | 48x48 dp | ❌ 需手动设置 |
颜色对比度
| 用途 | 最小对比度 | 推荐颜色 |
|---|---|---|
| 正文 | 4.5:1 | onSurface |
| 标题 | 4.5:1 | onSurface |
| 次要文字 | 4.5:1 | onSurfaceVariant |
| 禁用文字 | 可低于 4.5 | onSurface.withOpacity(0.38) |
语义化标签
| 组件 | 属性 | 示例 |
|---|---|---|
| Icon | semanticsLabel | semanticsLabel: '收藏' |
| Image | semanticsLabel | semanticsLabel: 'Logo' |
| IconButton | tooltip | tooltip: '删除' |
| 自定义 | Semantics | Semantics(label: '...') |
参考资料:
维护者:Reading Vocab Helper Team 问题反馈:请在项目中提 Issue