Skip to content

无障碍设计规范

最后更新:2026-02-21 版本:v1.0 状态:✅ 强制执行

本文档定义 Reading Vocab Helper 项目的 无障碍(Accessibility)设计规范,包括触摸目标、颜色对比度、语义化标签、屏幕阅读器兼容等。


📋 目录

  1. 触摸目标最小尺寸
  2. 颜色对比度
  3. 语义化标签
  4. 屏幕阅读器兼容
  5. 焦点管理
  6. 检查清单

触摸目标最小尺寸

核心原则:所有可点击元素的触摸目标 ≥ 48x48 dp

WCAG 和 Material 3 要求

标准最小尺寸推荐尺寸
WCAG 2.1 Level AA44x44 px48x48 px
Material 348x48 dp48x48 dp
iOS HIG44x44 pt44x44 pt

Flutter 默认行为

✅ 自动符合的组件

组件触摸区域说明
IconButton48x48 dp默认符合
ElevatedButton高度 40dp,宽度自适应需检查宽度
FilledButton高度 40dp,宽度自适应需检查宽度
TextButton高度 40dp,宽度自适应需检查宽度
Checkbox48x48 dp(包含 padding)默认符合
Radio48x48 dp(包含 padding)默认符合
Switch48x48 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 触摸区域
)

密集布局例外

在某些密集布局场景(如数据表格),可以使用较小的触摸目标,但需要:

  1. 在文档中标注例外原因
  2. 提供替代访问方式(如键盘导航)
  3. 确保间距足够避免误触
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 已经设计为符合对比度要求:

前景色背景色对比度
onPrimaryprimary≥ 4.5:1 ✅
onSecondarysecondary≥ 4.5:1 ✅
onSurfacesurface≥ 4.5:1 ✅
onSurfaceVariantsurface≥ 4.5:1 ✅
onErrorerror≥ 4.5:1 ✅

危险组合

❌ 避免以下组合

前景色背景色对比度问题
tertiarysurface可能 < 4.5使用 onSurface
outlinesurface可能 < 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,但需要确保:

  1. 所有交互元素有明确的语义
  2. 阅读顺序符合视觉逻辑
  3. 动态内容变化被正确通知

常用 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

  1. 设置 → 辅助功能 → VoiceOver → 开启
  2. 在应用中滑动浏览,检查朗读内容

Android

  1. 设置 → 无障碍 → TalkBack → 开启
  2. 在应用中滑动浏览,检查朗读内容

焦点管理

核心原则:支持键盘导航和焦点控制

焦点顺序

焦点顺序应遵循视觉阅读顺序(通常从左到右,从上到下)。

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 默认
IconButton48x48 dp✅ 符合
Button高度 40dp⚠️ 检查宽度
Checkbox48x48 dp✅ 符合
ListTile高度 ≥ 48dp✅ 符合
自定义按钮48x48 dp❌ 需手动设置

颜色对比度

用途最小对比度推荐颜色
正文4.5:1onSurface
标题4.5:1onSurface
次要文字4.5:1onSurfaceVariant
禁用文字可低于 4.5onSurface.withOpacity(0.38)

语义化标签

组件属性示例
IconsemanticsLabelsemanticsLabel: '收藏'
ImagesemanticsLabelsemanticsLabel: 'Logo'
IconButtontooltiptooltip: '删除'
自定义SemanticsSemantics(label: '...')

参考资料


维护者:Reading Vocab Helper Team 问题反馈:请在项目中提 Issue