Files
Obsidian/项目文档/徐工/语音/同行者可见即可说自动注册框架对接方案_快速阅读.md
T
2026-11-05 14:35:04 +08:00

468 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 同行者可见即可说自动注册框架对接方案 - 快速阅读版
> 来源文档:`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
<TextView
android:id="@+id/tv_daily_recommend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:text="每日推荐" />
```
支持泛化说法时,可使用 `contentDescription`:
```xml
<TextView
android:id="@+id/tv_daily_recommend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:text="每日推荐"
android:contentDescription="每日推荐#打开每日推荐#我想打开每日推荐" />
```
注意:
- `contentDescription` 中的插词文本必须包含 `text` 中的文本。
- 控件要能响应点击,需确保 `clickable=true` 或实际可点击。
## 4.2 图标类控件
适用对象:只有图片、没有文本的按钮或图标。
```xml
<ImageView
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="搜索#查找" />
```
注意:
- 图标没有显示文本时,必须用 `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
<RecyclerView
android:id="@+id/rv_station_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
```
item 示例:
```xml
<TextView
android:id="@+id/tv_station_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:text="88.6MHz"
android:contentDescription="88.6MHz#播放88.6" />
```
## 4.5 翻页控制
文档中特别强调:如果要让可见即可说支持“上一页 / 下一页”这类翻页逻辑,应用需要预留两个大小为 0 的控件。
要求:
- 控件大小为 0。
- 不能设置为 `GONE`。
- 分别添加“上一页”“下一页”等 `contentDescription`。
- 应用自己实现点击事件,点击后控制对应滚动控件翻页。
- 如果页面中有多个同方向滚动控件,只建议配置用户最可能使用的那个。
- 多组 0 大小翻页控件可能导致目标滚动控件不符合预期。
示例:
```xml
<View
android:id="@+id/voice_page_prev"
android:layout_width="0dp"
android:layout_height="0dp"
android:clickable="true"
android:contentDescription="上一页" />
<View
android:id="@+id/voice_page_next"
android:layout_width="0dp"
android:layout_height="0dp"
android:clickable="true"
android:contentDescription="下一页" />
```
```java
voicePagePrev.setOnClickListener(v -> recyclerView.smoothScrollBy(0, -recyclerView.getHeight()));
voicePageNext.setOnClickListener(v -> recyclerView.smoothScrollBy(0, recyclerView.getHeight()));
```
如果不预留翻页控制控件,默认不支持翻页逻辑。
## 4.6 SeekBar 等特殊滑动控件
适用对象:亮度、音量、进度条等 `SeekBar` 控件。
接入目标:支持类似“调高屏幕亮度”“降低音量”“屏幕亮度调大一点”等语音指令。
推荐做法:
```xml
<SeekBar
android:id="@+id/sb_screen_brightness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:contentDescription="屏幕亮度" />
```
父布局建议:
```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` 到实际业务动作的映射表。
- 现场日志验证结果。