前言
针对 Compose UI 工具包,开发者不仅需要掌握如何使用新的 UI 组件达到 design 需求,更需要了解和实现与 UI 的交互逻辑。
比如 touch 事件、Accessibility 事件等等。
- Compose 中对 touch 事件的处理和原理,笔者已经在《通过调用栈快速探究 Compose 中 touch 事件的处理原理》里进行了阐述
- Compose 中对 Accessibility 事件的支持和基本原理,笔者已经在 《一文读懂 Compose 支持 Accessibility 无障碍的原理》 里进行了介绍
那么将两个话题相结合,不禁要好奇:利用 Accessibility 针对 Compose 界面模拟 touch 交互,是否真的有效,个中原理又如何?
本文将通过无障碍 DEMO 对 Google Compose 项目 Accompanist 中的 Horizontal Pager sample 模拟注入 Scroll 滚动事件,看下实际效果,并对原理链路进行剖析。
向 Compose 模拟滚动事件
无障碍 DEMO,本来想直接复用曾经红极一时的 AccessibilityTool
开源项目。奈何代码太老编译不过,遂直接写了个 DEMO 来捕捉 AccessibilityEvent 然后分析 AccessibilityNodeInfo
。
当发现是节点属于 Accompanist 的包名(com.google.accompanist.sample),且可滚动 scrollable 的话,通过无障碍模拟注入 ACTION_SCROLL_FORWARD 的 action。
public class MyAccessibilityService extends AccessibilityService {
...
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.i(TAG, "onAccessibilityEvent() event: " + event);
AccessibilityNodeInfo root;
ArrayList<AccessibilityNodeInfo> roots = new ArrayList<>();
ArrayList<AccessibilityNodeInfo> nodeList = new ArrayList<>();
try {
switch (event.getEventType()) {
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
Log.i(TAG, "TYPE_WINDOW_STATE_CHANGED()");
roots.add(service.getRootInActiveWindow());
findAllNode(roots, nodeList);
printComposeNode(nodeList);
roots.clear();
nodeList.clear();
break;
...
}
} catch (Throwable e) {
e.printStackTrace();
}
}
private void printComposeNode(ArrayList<AccessibilityNodeInfo> root) {
for (AccessibilityNodeInfo node : root) {
if (node.getPackageName().equals("com.google.accompanist.sample")
&& node.getClassName().equals("android.view.View")) {
node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
}
}
...
}
《一文读懂 Compose 支持 Accessibility 无障碍的原理》 里我们介绍过,Compose 通过无障碍代理 AccessibilityDelegate
依据 UI 组件的类型、情况,进行 AccessibilityNodeInfo
实例的构造。
为了兼容传统 View 的内容,会针对实例里的 className 属性进行一定程度的了改写,但范围有限。
像 LazyColumn
这种的组件,并没有和传统的可滚动的 ListView
、ScrollView
、RecylerView
的名称进行转换,用的仍然是默认的 View
名称。
所以咱们的无障碍 DEMO 不能像以前那样在判断 isScrollable 之外再额外判断 ListView 等传统可滚动 View 的名称了。
话不多说,我们将无障碍 DEMO 在系统的无障碍设置中启用,选择 “allow” 即可。

然后运行下 Accompanist 的 Horizontal Pager 界面,打印下收集到的 AccessibilityNodeInfo 信息。
android.view.accessibility.AccessibilityNodeInfo@1cfed; ...
packageName: com.google.accompanist.sample; className: android.view.View; ...
enabled: true; ... scrollable: true; ...
actions: [AccessibilityAction: ... AccessibilityAction: ACTION_SCROLL_FORWARD - null]...
可以看到:
- className 果然是 android.view.View
- scrollable 是 true
- 支持的 AccessibilityAction 有 ACTION_SCROLL_FORWARD 等
模拟滚动的效果如下,可以看到一打开 Horizontal Pager 的界面,就自动往右进行了翻页。

Compose 支持模拟滚动的原理
滚动界面 Horizontal Pager
想了解 Compose 支持通过无障碍模拟滚动的原理,首先需要了解一下 Horizontal Pager 界面的布局和物理手势上触发滚动的一些背景知识。

该布局主要采用 TopAppBar
展示 Title 栏,内容区域由 Column
组件堆叠。其中:
ScrollableTabRow
负责可以横向滚动的 Tab 栏的内容展示HorizontalPager
负责各 Tab 对应内容的展示,会依据 page index 展示对应的 Text 文本,还需要监听 scroll 手势进行横向滚动
ScrollableTabRow 还需要监听 Tab 的点击事件进行 PagerState
的滚动,采用 animateScrollToPage() 进行。
class HorizontalPagerTabsSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {