15 KiB
同行者可见即可说自动注册框架对接方案 - 快速阅读版
来源文档:
F:/AI/Obsidian/项目文档/徐工/语音/同行者可见即可说自动注册框架对接方案.docx整理日期:2026-06-13 目标:把原 Word 文档整理成工程接入时能快速查阅的 Markdown 速查文档。
1. 一句话结论
“可见即可说”主要依赖 Android 无障碍服务扫描当前界面控件,通过控件的 android:id、android:text、android:contentDescription、clickable、scrollable 等信息自动注册语音热词。用户说出界面上可见或已通过隐藏标签声明的词后,语音助手命中热词,再回调触发目标控件事件。
如果界面元素无法被无障碍扫描,例如小程序、网页、自绘界面,则需要应用主动通过 AIDL 或广播向语音发送插词列表,语音命中后再把命中的插词回传给应用,由应用自行执行动作。
2. 整体链路
2.1 普通 Android 控件链路
- 应用界面展示控件。
- Android 无障碍服务扫描界面控件。
- 语音助手读取控件文本或隐藏标签。
- 语音助手把文本注册为可见即可说热词。
- 用户说出对应词,例如“每日推荐”或“打开每日推荐”。
- 热词模块命中控件节点。
- 无障碍服务触发控件点击或滑动等事件。
- 应用执行原控件逻辑。
2.2 图标控件链路
图标本身不显示文字时,需要通过 contentDescription 提供隐藏标签。例如图标按钮没有文字,但设置了 android:contentDescription="向下滑",语音仍可识别“向下滑”并触发该图标。
2.3 非常规界面链路
适用于小程序、网页、自绘页面等无障碍拿不到元素的场景。
- 应用整理当前界面可执行插词列表。
- 应用通过 AIDL 或广播发送给语音。
- 语音收到后注册插词。
- 用户说出插词。
- 语音识别命中。
- 语音通过 AIDL 或广播把命中插词回传给应用。
- 应用根据插词匹配具体控件或业务动作。
3. 接入总原则
| 原则 | 说明 |
|---|---|
| 必须有唯一 ID | 所有需要语音响应的控件都应设置唯一 android:id。 |
| 文本优先 | 文本控件优先使用 android:text 暴露插词。 |
| 图标补标签 | 图标类控件使用 android:contentDescription 暴露隐藏插词。 |
多说法用 # |
多个插词使用 # 分隔,例如 打开#启动。 |
| 状态要同步 | 播放/暂停、收藏/取消收藏等状态型图标必须动态更新 contentDescription。 |
点击看 clickable |
控件是否响应与 clickable 状态有关,true 表示可点击。 |
滚动看 scrollable |
滑动控件本身需要是可滑动节点,即 Scrollable=true。 |
| 弹窗用标准 Dialog | 建议使用标准 Dialog 或依附 Activity 的自定义 Dialog。 |
4. 常见控件接入方式
4.1 文本类控件
适用对象:TextView、Button、带文本的布局项等。
推荐做法:
<TextView
android:id="@+id/tv_daily_recommend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:text="每日推荐" />
支持泛化说法时,可使用 contentDescription:
<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 图标类控件
适用对象:只有图片、没有文本的按钮或图标。
<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 同一图标多状态控件
典型场景:播放/暂停、收藏/取消收藏。
错误做法:
android:contentDescription="播放#暂停"
这种做法可能导致当前处于暂停状态时,用户说“暂停”反而触发点击,结果开始播放。
正确做法:根据状态动态更新。
if (isPlaying) {
playButton.setContentDescription("暂停");
} else {
playButton.setContentDescription("播放");
}
原则:
- 当前正在播放,插词应是“暂停”。
- 当前已经暂停,插词应是“播放”。
- 当前已收藏,插词应是“取消收藏”。
- 当前未收藏,插词应是“收藏”。
4.4 常规滑动控件
适用对象:ScrollView、RecyclerView、ListView 等。
基础要求:
- 控件本身应可滑动,即无障碍节点
Scrollable=true。 - 如果每个 item 需要响应语音,item 需要有唯一 ID。
- item 可使用
contentDescription声明插词。 - 如需响应 item 内部 child 控件,child 按文本类或图标类控件处理。
示例:
<RecyclerView
android:id="@+id/rv_station_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
item 示例:
<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 大小翻页控件可能导致目标滚动控件不符合预期。
示例:
<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="下一页" />
voicePagePrev.setOnClickListener(v -> recyclerView.smoothScrollBy(0, -recyclerView.getHeight()));
voicePageNext.setOnClickListener(v -> recyclerView.smoothScrollBy(0, recyclerView.getHeight()));
如果不预留翻页控制控件,默认不支持翻页逻辑。
4.6 SeekBar 等特殊滑动控件
适用对象:亮度、音量、进度条等 SeekBar 控件。
接入目标:支持类似“调高屏幕亮度”“降低音量”“屏幕亮度调大一点”等语音指令。
推荐做法:
<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="屏幕亮度" />
父布局建议:
android:clipChildren="false"
android:clipToPadding="false"
原因:
- 框架根据控件宽高计算百分比拖动距离。
- 无法直接读取控件内部业务值。
SeekBar默认内边距会造成拖动精度误差。- 将
SeekBar内边距设为 0,并允许子控件超出父布局边界,可减少误差。
4.7 Switch 开关类控件
原生 Switch:通常无需额外适配。
自定义开关 View:需要重写 onInitializeAccessibilityNodeInfo。
@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 |
插词列表,泛化词用 # 分隔 |
打开#启动 |
示例:
{
"key_type": 2600,
"action": "shadow.register",
"packageName": "com.yaxon.carcontrol",
"className": "com.yaxon.carcontrol.activity.CarControlMainActivity",
"pageName": "voiceSetting",
"keywordList": "打开#启动"
}
取消注册:
{
"key_type": 2600,
"action": "shadow.unregister",
"packageName": "com.yaxon.carcontrol",
"pageName": "voiceSetting",
"keywordList": "打开#启动"
}
5.2 语音回传给应用:命中执行
| 字段 | 含义 | 示例 |
|---|---|---|
key_type |
回传类型,1600 表示可见即可说执行 |
1600 |
action |
动作 | shadow.execute |
cmd |
命中的插词原文 | 打开 |
示例:
{
"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 重新显示时,再恢复文本和隐藏标签。
示例:
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. 当前徐工项目建议落地顺序
- 先验证
YX_Voice与同行者之间的shadow.register/shadow.execute链路。 - 在
YX_CarControl做一个测试插词,不绑定真实业务动作,只看日志是否能完整闭环。 - 链路确认后,再选一个真实页面做最小闭环,例如语音设置页或声音设置页。
- 对原生 Android 控件,优先走无障碍自动扫描,不额外走 AIDL/广播。
- 对自绘、网页、小程序、复杂浮窗,走 AIDL/广播注册插词。
- 对 SystemUI、Launcher、Music、Radio 这类插词多且易冲突的模块,先做页面级插词清单,再逐个治理冲突。
9. 日志验证建议
注册阶段重点看:
registerVisibleSpeakKeywords
shadow.register
keywordList
命中阶段重点看:
shadow.execute
cmd
handleVisibleSpeakExecute
异常排查重点看:
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到实际业务动作的映射表。 - 现场日志验证结果。