跳转到正文
YGJoon 技术博客

Flutter无障碍开发指南

发布于:
·1.4k 字 · 约 4 分钟·作者:
Semantics 语义标签、焦点管理、MergeSemantics 合并朗读,让 TalkBack 与 VoiceOver 正确播报你的界面
Flutter无障碍开发指南文章封面
目录

简介

无障碍(Accessibility)功能对于构建包容性应用至关重要,本文将详细介绍如何在Flutter应用中实现无障碍支持,包括语音辅助、高对比度、焦点导航等特性。

基础配置

1. 语义标签

class AccessibleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Semantics(
      label: '这是一个按钮',
      hint: '点击执行某个操作',
      button: true,
      child: ElevatedButton(
        onPressed: () {},
        child: Text('点击'),
      ),
    );
  }
}

2. 焦点管理

class FocusableWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Focus(
      autofocus: true,
      child: TextFormField(
        decoration: InputDecoration(
          labelText: '输入框',
          semanticCounterText: '这是一个文本输入框',
        ),
      ),
    );
  }
}

语音辅助功能

1. TalkBack/VoiceOver支持

class VoiceAssistantWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ExcludeSemantics(
      excluding: false,
      child: MergeSemantics(
        child: Column(
          children: [
            Text(
              '标题文本',
              semanticsLabel: '这是页面的主标题',
            ),
            Image.asset(
              'assets/image.png',
              semanticLabel: '这是一张示例图片',
            ),
          ],
        ),
      ),
    );
  }
}

2. 自定义语音描述

class CustomSemantics extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: MyCustomPainter(),
      child: Semantics(
        label: '自定义图形',
        value: '这是一个圆形',
        onTapHint: '点击查看详情',
        child: Container(
          width: 100,
          height: 100,
        ),
      ),
    );
  }
}

高对比度支持

1. 颜色对比度检查

class ContrastAwareWidget extends StatelessWidget {
  Color getAccessibleTextColor(Color background) {
    // 计算对比度
    double luminance = background.computeLuminance();
    return luminance > 0.5 ? Colors.black : Colors.white;
  }

  @override
  Widget build(BuildContext context) {
    final backgroundColor = Theme.of(context).primaryColor;
    final textColor = getAccessibleTextColor(backgroundColor);

    return Container(
      color: backgroundColor,
      child: Text(
        '高对比度文本',
        style: TextStyle(color: textColor),
      ),
    );
  }
}

2. 主题适配

class AccessibleTheme {
  static ThemeData getAccessibleTheme(bool highContrast) {
    return ThemeData(
      primaryColor: highContrast ? Colors.black : Colors.blue,
      textTheme: TextTheme(
        bodyLarge: TextStyle(
          color: highContrast ? Colors.white : Colors.black87,
          fontSize: 16,
        ),
      ),
      // 其他主题配置
    );
  }
}

焦点导航

1. 焦点遍历顺序

class FocusTraversalWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FocusTraversalGroup(
      policy: ReadingOrderTraversalPolicy(),
      child: Column(
        children: [
          Focus(
            child: TextButton(
              onPressed: () {},
              child: Text('第一个按钮'),
            ),
          ),
          Focus(
            child: TextButton(
              onPressed: () {},
              child: Text('第二个按钮'),
            ),
          ),
        ],
      ),
    );
  }
}

2. 自定义焦点行为

class CustomFocusWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FocusScope(
      onFocusChange: (focused) {
        // 处理焦点变化
      },
      child: KeyboardListener(
        onKeyEvent: (event) {
          // 处理键盘事件
        },
        child: YourWidget(),
      ),
    );
  }
}

手势识别

1. 基础手势支持

class AccessibleGestureWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Semantics(
      onTapHint: '双击打开',
      onLongPressHint: '长按删除',
      child: GestureDetector(
        onDoubleTap: () {
          // 处理双击事件
        },
        onLongPress: () {
          // 处理长按事件
        },
        child: Container(
          padding: EdgeInsets.all(16),
          child: Text('手势示例'),
        ),
      ),
    );
  }
}

2. 自定义手势

class CustomGestureWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<AllowMultipleGestureRecognizer>(
          () => AllowMultipleGestureRecognizer(),
          (AllowMultipleGestureRecognizer instance) {
            instance.onTap = () {
              // 处理自定义手势
            };
          },
        ),
      },
      child: YourWidget(),
    );
  }
}

测试与验证

1. 无障碍测试

void main() {
  testWidgets('无障碍功能测试', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    final SemanticsHandle handle = tester.ensureSemantics();

    // 验证语义标签
    expect(
      tester.getSemantics(find.byType(MyWidget)),
      matchesSemantics(
        label: '预期的标签',
        isButton: true,
        isFocusable: true,
      ),
    );

    handle.dispose();
  });
}

2. 对比度测试

class ContrastChecker {
  static double calculateContrast(Color foreground, Color background) {
    double fgLuminance = foreground.computeLuminance();
    double bgLuminance = background.computeLuminance();

    double brighter = max(fgLuminance, bgLuminance);
    double darker = min(fgLuminance, bgLuminance);

    return (brighter + 0.05) / (darker + 0.05);
  }

  static bool isAccessible(Color foreground, Color background) {
    double ratio = calculateContrast(foreground, background);
    return ratio >= 4.5; // WCAG 2.0 AA标准
  }
}

最佳实践

1. 对比度按 WCAG AA 的线卡:正文 4.5:1,大文字 3:1

浅灰字配白色底在设计稿上好看,视障用户和阳光下看手机的用户都看不清。用工具量一下,不达标就加深字色——这是无障碍里投入产出比最高的一条。

2. 可点击区域不小于 48×48 dp

Material 无障碍规范的硬线。16px 的小图标按钮看着精致,手指点不中就是反人类。视觉可以小,点击区域用 SizedBox 或 padding 撑到 48。

3. 别只用颜色传达信息

“红色表示失败、绿色表示成功”对红绿色盲用户等于没说。状态一定要配文字或图标双通道:颜色 + 文案,缺一不可。

4. 读屏能走通核心流程就算及格

打开 TalkBack/VoiceOver,从首页走到下单/核心操作,能听清每个按钮是干嘛的、焦点顺序不乱跳,就是及格线。Semantics 的 label 和 excludeSemantics 是主要工具——先保证核心流程,再谈全量覆盖。

实战案例

无障碍(a11y)我还没在项目里系统做过——我的 App 没收到过视障用户的反馈,Semantics widget 一个没写过。诚实说,这是欠账。但我知道它的”及格线”很低:给图片加 semanticLabel、按钮保证有文字(别只用图标)、颜色对比度别太离谱,这三条做了就超过 80% 的 App。Flutter 的无障碍支持其实不错(Semantics + 系统读屏自动打通),成本不高。我的计划是:下个版本给 flutter_travel 的核心流程(选城市、下单)补上 semanticLabel。无障碍不是”爱心工程”,是”迟早要还的技术债”——等应用商店强制要求那天再补就晚了。参考:Flutter 官方文档无障碍章节。

总结

完善的无障碍支持能够:

  1. 提升应用的可访问性
  2. 扩大用户群体
  3. 提高用户满意度
  4. 符合法规要求

通过合理的实现和优化,可以:


分享这篇文章:


相关文章

上一篇
Flutter 依赖注入详解
下一篇
Flutter 多线程和并发编程