first commit

This commit is contained in:
admin committed 2026-11-05 14:35:04 +08:00
commit 669f49b1d6
342 files changed
+320811

No files matched your search

@@ -0,0 +1,467 @@
# 同行者可见即可说自动注册框架对接方案 - 快速阅读版
> 来源文档:`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` 到实际业务动作的映射表。
- 现场日志验证结果。