# 同行者可见即可说自动注册框架对接方案 - 快速阅读版 > 来源文档:`F:/AI/Obsidian/项目文档/徐工/语音/同行者可见即可说自动注册框架对接方案.docx` > 整理日期:2026-06-13 > 目标:把原 Word 文档整理成工程接入时能快速查阅的 Markdown 速查文档。 ## 1. 一句话结论 “可见即可说”主要依赖 Android 无障碍服务扫描当前界面控件,通过控件的 `android:id`、`android:text`、`android:contentDescription`、`clickable`、`scrollable` 等信息自动注册语音热词。用户说出界面上可见或已通过隐藏标签声明的词后,语音助手命中热词,再回调触发目标控件事件。 如果界面元素无法被无障碍扫描,例如小程序、网页、自绘界面,则需要应用主动通过 AIDL 或广播向语音发送插词列表,语音命中后再把命中的插词回传给应用,由应用自行执行动作。 ## 2. 整体链路 ### 2.1 普通 Android 控件链路 1. 应用界面展示控件。 2. Android 无障碍服务扫描界面控件。 3. 语音助手读取控件文本或隐藏标签。 4. 语音助手把文本注册为可见即可说热词。 5. 用户说出对应词,例如“每日推荐”或“打开每日推荐”。 6. 热词模块命中控件节点。 7. 无障碍服务触发控件点击或滑动等事件。 8. 应用执行原控件逻辑。 ### 2.2 图标控件链路 图标本身不显示文字时,需要通过 `contentDescription` 提供隐藏标签。例如图标按钮没有文字,但设置了 `android:contentDescription="向下滑"`,语音仍可识别“向下滑”并触发该图标。 ### 2.3 非常规界面链路 适用于小程序、网页、自绘页面等无障碍拿不到元素的场景。 1. 应用整理当前界面可执行插词列表。 2. 应用通过 AIDL 或广播发送给语音。 3. 语音收到后注册插词。 4. 用户说出插词。 5. 语音识别命中。 6. 语音通过 AIDL 或广播把命中插词回传给应用。 7. 应用根据插词匹配具体控件或业务动作。 ## 3. 接入总原则 | 原则 | 说明 | |---|---| | 必须有唯一 ID | 所有需要语音响应的控件都应设置唯一 `android:id`。 | | 文本优先 | 文本控件优先使用 `android:text` 暴露插词。 | | 图标补标签 | 图标类控件使用 `android:contentDescription` 暴露隐藏插词。 | | 多说法用 `#` | 多个插词使用 `#` 分隔,例如 `打开#启动`。 | | 状态要同步 | 播放/暂停、收藏/取消收藏等状态型图标必须动态更新 `contentDescription`。 | | 点击看 `clickable` | 控件是否响应与 `clickable` 状态有关,`true` 表示可点击。 | | 滚动看 `scrollable` | 滑动控件本身需要是可滑动节点,即 `Scrollable=true`。 | | 弹窗用标准 Dialog | 建议使用标准 `Dialog` 或依附 Activity 的自定义 Dialog。 | ## 4. 常见控件接入方式 ## 4.1 文本类控件 适用对象:`TextView`、`Button`、带文本的布局项等。 推荐做法: ```xml ``` 支持泛化说法时,可使用 `contentDescription`: ```xml ``` 注意: - `contentDescription` 中的插词文本必须包含 `text` 中的文本。 - 控件要能响应点击,需确保 `clickable=true` 或实际可点击。 ## 4.2 图标类控件 适用对象:只有图片、没有文本的按钮或图标。 ```xml ``` 注意: - 图标没有显示文本时,必须用 `contentDescription` 提供语音插词。 - 多个插词用 `#` 分隔。 - 控件是否响应仍与 `clickable` 状态有关。 ## 4.3 同一图标多状态控件 典型场景:播放/暂停、收藏/取消收藏。 错误做法: ```xml android:contentDescription="播放#暂停" ``` 这种做法可能导致当前处于暂停状态时,用户说“暂停”反而触发点击,结果开始播放。 正确做法:根据状态动态更新。 ```java if (isPlaying) { playButton.setContentDescription("暂停"); } else { playButton.setContentDescription("播放"); } ``` 原则: - 当前正在播放,插词应是“暂停”。 - 当前已经暂停,插词应是“播放”。 - 当前已收藏,插词应是“取消收藏”。 - 当前未收藏,插词应是“收藏”。 ## 4.4 常规滑动控件 适用对象:`ScrollView`、`RecyclerView`、`ListView` 等。 基础要求: - 控件本身应可滑动,即无障碍节点 `Scrollable=true`。 - 如果每个 item 需要响应语音,item 需要有唯一 ID。 - item 可使用 `contentDescription` 声明插词。 - 如需响应 item 内部 child 控件,child 按文本类或图标类控件处理。 示例: ```xml ``` item 示例: ```xml ``` ## 4.5 翻页控制 文档中特别强调:如果要让可见即可说支持“上一页 / 下一页”这类翻页逻辑,应用需要预留两个大小为 0 的控件。 要求: - 控件大小为 0。 - 不能设置为 `GONE`。 - 分别添加“上一页”“下一页”等 `contentDescription`。 - 应用自己实现点击事件,点击后控制对应滚动控件翻页。 - 如果页面中有多个同方向滚动控件,只建议配置用户最可能使用的那个。 - 多组 0 大小翻页控件可能导致目标滚动控件不符合预期。 示例: ```xml ``` ```java voicePagePrev.setOnClickListener(v -> recyclerView.smoothScrollBy(0, -recyclerView.getHeight())); voicePageNext.setOnClickListener(v -> recyclerView.smoothScrollBy(0, recyclerView.getHeight())); ``` 如果不预留翻页控制控件,默认不支持翻页逻辑。 ## 4.6 SeekBar 等特殊滑动控件 适用对象:亮度、音量、进度条等 `SeekBar` 控件。 接入目标:支持类似“调高屏幕亮度”“降低音量”“屏幕亮度调大一点”等语音指令。 推荐做法: ```xml ``` 父布局建议: ```xml android:clipChildren="false" android:clipToPadding="false" ``` 原因: - 框架根据控件宽高计算百分比拖动距离。 - 无法直接读取控件内部业务值。 - `SeekBar` 默认内边距会造成拖动精度误差。 - 将 `SeekBar` 内边距设为 0,并允许子控件超出父布局边界,可减少误差。 ## 4.7 Switch 开关类控件 原生 `Switch`:通常无需额外适配。 自定义开关 View:需要重写 `onInitializeAccessibilityNodeInfo`。 ```java @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setCheckable(true); info.setChecked(isChecked()); } ``` 要求: - 设置 `checkable=true`。 - 根据真实开关状态同步 `checked`。 - 状态变化时及时刷新无障碍信息。 ## 5. 非常规界面 AIDL / 广播对接字段 文档流程图给出的字段含义如下。 ## 5.1 应用发送给语音:注册或取消注册 | 字段 | 含义 | 示例 | | ------------- | --------------------- | ------------------------------------------------------ | | `key_type` | 要处理的类型,`2600` 表示可见即可说 | `2600` | | `action` | 动作 | `shadow.register` / `shadow.unregister` | | `packageName` | 应用包名,可不填 | `com.yaxon.carcontrol` | | `className` | 页面类名,可不填 | `com.yaxon.carcontrol.activity.CarControlMainActivity` | | `pageName` | 页面名称,可不填 | `voiceSetting` | | `keywordList` | 插词列表,泛化词用 `#` 分隔 | `打开#启动` | 示例: ```json { "key_type": 2600, "action": "shadow.register", "packageName": "com.yaxon.carcontrol", "className": "com.yaxon.carcontrol.activity.CarControlMainActivity", "pageName": "voiceSetting", "keywordList": "打开#启动" } ``` 取消注册: ```json { "key_type": 2600, "action": "shadow.unregister", "packageName": "com.yaxon.carcontrol", "pageName": "voiceSetting", "keywordList": "打开#启动" } ``` ## 5.2 语音回传给应用:命中执行 | 字段 | 含义 | 示例 | | ---------- | --------------------- | ---------------- | | `key_type` | 回传类型,`1600` 表示可见即可说执行 | `1600` | | `action` | 动作 | `shadow.execute` | | `cmd` | 命中的插词原文 | `打开` | 示例: ```json { "key_type": 1600, "action": "shadow.execute", "cmd": "打开" } ``` 应用收到后,应根据 `cmd` 做业务分发。 ## 6. 已知问题与规避方式 ## 6.1 `com.android.systemui` 扫描冲突 问题: - 底部 touch 栏中可能存在很多图标插词。 - 这些插词可能和桌面或其他应用重复,例如“车主手册”。 - `com.android.systemui` 所在 window 可能一直可见,即使视觉上隐藏,也可能被无障碍服务扫描到。 - 用户说“车主手册”时,可能命中 SystemUI 上隐藏的控件,导致结果不符合预期。 建议: - SystemUI 隐藏时,临时清空相关控件的 `android:text` 和 `android:contentDescription`。 - SystemUI 重新显示时,再恢复文本和隐藏标签。 示例: ```java if (isSystemUiHidden) { view.setContentDescription(null); textView.setText(""); } else { view.setContentDescription("车主手册"); textView.setText("车主手册"); } ``` ## 6.2 同一图片不同插词 问题: - 同一图片可能对应不同状态,例如播放/暂停。 - 如果同时注册“播放#暂停”,当前状态下可能执行反向动作。 建议: - 状态变化时动态更新 `contentDescription`。 - 当前能执行什么动作,就只暴露什么插词。 ## 6.3 部分弹框扫描不到 问题: - 一些通过 Window 直接创建的弹框,可能与 Activity 界面不匹配或层级冲突。 - 无障碍服务可能扫描不到弹框里的控件。 建议: - 优先使用标准 `Dialog`。 - 自定义 Dialog 应依附于 Activity。 - 避免直接用独立 Window 承载需要语音响应的弹框。 ## 7. 工程接入检查清单 ## 7.1 页面级检查 - [ ] 页面中需要语音响应的控件是否都有唯一 `android:id`。 - [ ] 文本控件是否有明确 `android:text`。 - [ ] 图标控件是否设置了 `android:contentDescription`。 - [ ] 多说法是否使用 `#` 分隔。 - [ ] 控件是否实际可点击或可滑动。 - [ ] 动态状态控件是否会更新 `contentDescription`。 - [ ] 列表 item 是否有唯一 ID 或稳定的可识别标签。 - [ ] 滚动页面是否需要预留上一页/下一页控制控件。 - [ ] 弹框是否使用标准 Dialog 或依附 Activity。 ## 7.2 状态型控件检查 - [ ] 播放状态:播放中只暴露“暂停”。 - [ ] 暂停状态:暂停中只暴露“播放”。 - [ ] 已收藏:只暴露“取消收藏”。 - [ ] 未收藏:只暴露“收藏”。 - [ ] 开关类自定义 View 是否正确设置 `checkable` 和 `checked`。 ## 7.3 SystemUI 检查 - [ ] 隐藏状态是否仍可能被无障碍扫描。 - [ ] 隐藏时是否清空 `text/contentDescription`。 - [ ] 显示时是否恢复 `text/contentDescription`。 - [ ] 是否存在与 Launcher 或 App 页面重复的插词。 ## 7.4 非常规界面检查 - [ ] 是否确认无障碍无法获取页面元素。 - [ ] 是否整理了当前页面插词列表。 - [ ] 是否支持注册和取消注册。 - [ ] 是否能接收语音命中的 `cmd`。 - [ ] `cmd` 到业务动作是否有明确映射表。 - [ ] 页面销毁或切换时是否取消旧插词。 ## 8. 当前徐工项目建议落地顺序 1. 先验证 `YX_Voice` 与同行者之间的 `shadow.register` / `shadow.execute` 链路。 2. 在 `YX_CarControl` 做一个测试插词,不绑定真实业务动作,只看日志是否能完整闭环。 3. 链路确认后,再选一个真实页面做最小闭环,例如语音设置页或声音设置页。 4. 对原生 Android 控件,优先走无障碍自动扫描,不额外走 AIDL/广播。 5. 对自绘、网页、小程序、复杂浮窗,走 AIDL/广播注册插词。 6. 对 SystemUI、Launcher、Music、Radio 这类插词多且易冲突的模块,先做页面级插词清单,再逐个治理冲突。 ## 9. 日志验证建议 注册阶段重点看: ```text registerVisibleSpeakKeywords shadow.register keywordList ``` 命中阶段重点看: ```text shadow.execute cmd handleVisibleSpeakExecute ``` 异常排查重点看: ```text keywordList is empty TXZ not connected protocolLogic is null packageName or cmd is empty ``` ## 10. 快速判断表 | 现象 | 优先排查 | | ------------ | --------------------------------------------- | | 说了没反应 | 控件是否有 ID、是否 clickable、是否注册成功 | | 图标不能说 | 是否设置 `contentDescription` | | 状态动作反了 | 是否动态更新 `contentDescription` | | 列表不能翻页 | 是否预留 0 大小上一页/下一页控件 | | SeekBar 拖动不准 | 是否去掉 SeekBar 内边距,父布局是否设置 `clipChildren=false` | | 弹框扫不到 | 是否用标准 Dialog 或依附 Activity | | 命中到错误应用 | 是否存在 SystemUI / Launcher / 当前页面插词冲突 | | 非常规页面无元素 | 改走 AIDL 或广播插词注册 | ## 11. 后续需求明确后需要补充的内容 - 每个模块的可见即可说页面清单。 - 每个页面的控件 ID、显示文本、隐藏标签、状态规则。 - 插词冲突表,尤其是 SystemUI、Launcher、CarControl、Music、Radio。 - 非常规界面的 AIDL / 广播接口定义。 - 命中 `cmd` 到实际业务动作的映射表。 - 现场日志验证结果。