/// ============================================================ /// 闲言APP — 无障碍服务 /// 创建时间: 2026-06-05 /// 更新时间: 2026-06-12 /// 作用: 管理VoiceOver/TalkBack适配,提供无障碍模式检测、 /// 语义标注辅助、设置管理 /// 上次更新: 从 accessibility/ 子目录上移至 services/ 目录 /// ============================================================ import 'package:flutter/semantics.dart'; import 'package:flutter/widgets.dart'; import '../utils/logger.dart'; /// 无障碍服务 — 单例管理VoiceOver/TalkBack适配 /// /// 职责: /// - 检测系统无障碍状态(高对比度、减少动画、粗体文本) /// - 管理语义调试开关(SemanticsDebugger) /// - 提供语义标注辅助方法 class AccessibilityService { AccessibilityService._(); static AccessibilityService? _instance; static AccessibilityService get instance => _instance ??= AccessibilityService._(); // ── 状态字段 ── /// 是否启用语义调试(开发者选项) bool _semanticsDebugEnabled = false; /// 是否高对比度模式(系统级,只读) bool _highContrastEnabled = false; /// 是否减少动画(系统级,只读) bool _reducedMotionEnabled = false; /// 是否粗体文本(系统级,只读) bool _boldTextEnabled = false; // ── Getter ── /// 是否启用语义调试 bool get semanticsDebugEnabled => _semanticsDebugEnabled; /// 是否高对比度模式 bool get highContrastEnabled => _highContrastEnabled; /// 是否减少动画 bool get reducedMotionEnabled => _reducedMotionEnabled; /// 是否粗体文本 bool get boldTextEnabled => _boldTextEnabled; // ── 初始化与更新 ── /// 从BuildContext更新系统无障碍状态 /// /// 应在App根组件build时调用,以同步系统级无障碍设置 void updateFromContext(BuildContext context) { final mediaQuery = MediaQuery.of(context); final changed = _highContrastEnabled != mediaQuery.highContrast || _reducedMotionEnabled != mediaQuery.disableAnimations || _boldTextEnabled != mediaQuery.boldText; _highContrastEnabled = mediaQuery.highContrast; _reducedMotionEnabled = mediaQuery.disableAnimations; _boldTextEnabled = mediaQuery.boldText; if (changed) { Log.i( '无障碍状态更新: 高对比度=$_highContrastEnabled, ' '减少动画=$_reducedMotionEnabled, ' '粗体文本=$_boldTextEnabled', null, null, LogCategory.service, ); } } /// 初始化语义调试状态(从持久化存储恢复) void initSemanticsDebug(bool enabled) { _semanticsDebugEnabled = enabled; Log.i('无障碍服务初始化: 语义调试=$enabled', null, null, LogCategory.service); } /// 切换语义调试开关 /// /// 开启后会在应用最外层显示SemanticsDebugger覆盖层, /// 方便开发者检查语义标注是否正确 void setSemanticsDebug(bool enabled) { if (_semanticsDebugEnabled == enabled) return; _semanticsDebugEnabled = enabled; Log.i('语义调试切换: $enabled', null, null, LogCategory.service); // 通知语义调试状态变化 // ignore: deprecated_member_use SemanticsService.announce( enabled ? '语义调试已开启' : '语义调试已关闭', TextDirection.ltr, ); } // ── 语义标注辅助 ── /// 为卡片类组件生成语义标注字符串 /// /// [title] 卡片标题 /// [subtitle] 卡片副标题 /// [extra] 额外信息(如点赞数、评论数) static String cardLabel({ required String title, String? subtitle, String? extra, }) { final parts = [title]; if (subtitle != null && subtitle.isNotEmpty) parts.add(subtitle); if (extra != null && extra.isNotEmpty) parts.add(extra); return parts.join(','); } /// 为开关类组件生成语义hint static String toggleHint(String label, bool currentValue) { return currentValue ? '关闭$label' : '开启$label'; } /// 为列表项生成语义value static String listItemValue(int index, int total) { return '第${index + 1}项,共$total项'; } }