官方的uiautomator是没有xpath选择器的,这里介绍一种利用xpath查找控件的方法。
首要的问题是,如何获取界面元素根节点。
先来看UiDevice的这段代码:
public void dumpWindowHierarchy(String fileName) { AccessibilityNodeInfo root = getAutomatorBridge().getQueryController().getAccessibilityRootNode(); if(root != null) { AccessibilityNodeInfoDumper.dumpWindowToFile( root, new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName)); } }
root即是所要获取的根节点。
接下来看getAutomatorBridge()干了什么:
UiAutomatorBridge getAutomatorBridge() { return mUiAutomationBridge; }
很简单,直接返回了一个成员变量。我们看看这个变量是啥:
// provides access the {@link QueryController} and {@link InteractionController} private final UiAutomatorBridge mUiAutomationBridge;
getAutomatorBridge()方法是包内可见的,也就是说在代码中不能直接调用。就算定义了一个com.android.uiautomator.core包内的类,也没法调用,因为android.jar并没有对外暴露这个API.
不过可以通过反射的方式,获取UiDevice实例对象的getAutomatorBridge方法,再行调用就可以了。
另外,这个方法返回的是UiAutomatorBridge类型,这个类型也是包内可见,没问题,再用反射就是。
就这样一层一层剥,终于获取到AccessibilityNodeInfo类型的根节点,代码如下:
UiDevice dev = UiDevice.getInstance(); Method getAutomatorBridge = UiDevice.class.getDeclaredMethod("getAutomatorBridge"); Object bridge = getAutomatorBridge.invoke(dev); Class<?> UiAutomatorBridge_class = dev.getClass().getClassLoader().loadClass("com.android.uiautomator.core.UiAutomatorBridge"); Method getQueryController = UiAutomatorBridge_class.getDeclaredMethod("getQueryController"); Object qc = getQueryController.invoke(bridge); Class<?> QueryController_class = dev.getClass().getClassLoader().loadClass("com.android.uiautomator.core.QueryController"); Method getAccessibilityRootNode = QueryController_class.getDeclaredMethod("getAccessibilityRootNode"); AccessibilityNodeInfo root = (AccessibilityNodeInfo) getAccessibilityRootNode.invoke(qc);
获取界面根节点后,事情就好办了。参见这个类:com.android.uiautomator.core.AccessibilityNodeInfoDumper,它从根节点出发遍历控件树,读取控件属性,并生成对应的XML节点,过程很简单,不贴代码了。
需要注意的是,生成的XML除了根节点,每个节点名称都是node,控件类型则放在class属性中。可以重写这个过程,把class作为XML节点名称,这样就可以利用xpath进行定位了。
XML示例片断如下:
<hierarchy> <android.widget.FrameLayout instance="0" index="0" text="" resource-id="" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,1920]"> <android.widget.FrameLayout instance="1" index="0" text="" resource-id="miui:id/action_bar_overlay_layout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,1920]"> <android.widget.FrameLayout instance="2" index="0" text="" resource-id="android:id/content" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,1920]"><!-- 省略 --></hierarchy>
注意这里增加了一个很重要的属性instance,它对应于UiSelector.instance(int),表示同类型节点的深度优先次序编号。
在生成XML遍历控件树时,是以深度优先次序进行的,只要记住遍历过节点每种类型的个数,就能很方便地得到instance属性。
后面就简单了,用xpath定位节点,获取节点的名称和instance属性,就可以利用UiSelector.className(...).instance(...)进行控件定位。
还有一种笨方法,在xpath定位到节点后,一路沿parent向上,直到根节点,并把路径记下来。再从根节点出发,沿路径返回,利用子节点的index属性,通过UiObject.getChild(...)来依次获取子节点,就这样一路向下,直到目标节点。这个方法比较笨,个人觉得它只适合于前一种方法找不到的情况下的一种补充手段。