ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 1. 背景 在使用MonkeyRunner的时候我们经常会用到Chimchat下面的HierarchyViewer模块来获取目标控件的一些信息来辅助我们测试,但在MonkeyRunner的官网上是没有看到相应的API的描述的,上面只有以下三个类的API引用信息([http://developer.android.com/tools/help/MonkeyDevice.html](http://developer.android.com/tools/help/MonkeyDevice.html)) - MonkeyDevice - MonkeyImage - MonkeyRunner![](https://box.kancloud.cn/2016-08-15_57b1755741986.jpg) 所以在这里尝试整理下HierarchyViewer提供的API的用法并根据实践作出相应的建议,首先请看该类提供的所有可用的公共方法,内容并不多: ![](https://box.kancloud.cn/2016-08-15_57b175575f6ac.jpg) 从图中可以看出HierarchyViewer类中提供的方法主要是用来定位控件相关的,包括根据ID取得控件,根据控件取得控件在屏幕的位置等。但还有一些其他方法,我们会顺带一并描述,毕竟内容并不多。 本文我们依然跟上几篇文章一样以SDK自带的NotePad为实验目标,看怎么定位到NotesList下面的Menu Options中的Add note这个Menu Entry。 以下是通过HierarchyViewer这个工具获得的目标设备界面的截图:![](https://box.kancloud.cn/2016-08-15_57b1755778f2e.jpg) ## 2.findViewById(String id) ### 2.1 示例 ~~~ targetDevice = MonkeyRunner.waitForConnection() ''' public ViewNode findViewById(String id) * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. ''' viewer = targetDevice.getHierarchyViewer() button = viewer.findViewById('id/title') text = viewer.getText(button) print text.encode('utf-8') ~~~ ~~~ ~~~ ### 2.2 分析和建议 此API的目的就是通过控件的ID来获得代表用户控件的一个ViewNode对象。因为这个是第一个示例,所以这里有几点需要说明 - 一旦MonkeyRunner连接上设备,会立刻获得一个MonkeyDevice的对象代表了目标测试设备,我们就是通过这个设备对象来控制设备的 - 注意这里需要填写的id的格式和UIAutomatorViewer获得ResourceId是不一样的,请看下图UIAutomatorViewer截图中ResourceId前面多出了"android:"字串:![](https://box.kancloud.cn/2016-08-15_57b1755799a2f.jpg) - 这个方法返回的一个ViewNode的对象,代表目标控件,拥有大量控件相关的属性,由于篇幅问题这里不详述,往后应该会另外撰文描述它的使用。在本文里知道它代表了目标控件就行了 - 最后打印的时候需要转换成UTF-8编码的原因跟Jython默认的编码格式有关系,具体描述和Workaround请查看:[http://www.haogongju.net/art/1636997](http://www.haogongju.net/art/1636997) ## 3. findViewById(String id, ViewNode rootNode) ### 3.1示例 ~~~ ''' public ViewNode findViewById(String id, ViewNode rootNode) * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. ''' iconMenuView = viewer.findViewById('id/icon_menu') button = viewer.findViewById('id/title',iconMenuView) print "Button Text:",text.encode('utf-8') ~~~ ### 3.2分析 这个方法是上面方法的一个重载,除了需要指定ID之外,还需要指定一个rootNode,该rootNode指的就是已知控件的父控件,父到什么层级就没有限制了。为什么需要这个方法了,我们可以想象下这种情况:同一界面上存在两个控件拥有相同的ID,但是他们某一个层级父控件开始发生分叉。那么我们就可以把rootNode指定为该父控件(不包括)到目标控件(不包含)路径中的其中一个父控件来精确定位我们需要的目标控件了。 如我们的示例就是明确指出我们需要的是在父控件“id/icon_menu"(请看背景的hierarchyviewer截图)下面的那个”id/title"控件。 ## 4 getAbsolutePositionOfView(ViewNode node) ### 4.1示例 ~~~ ''' public static Point getAbsoluteCenterOfView(ViewNode node) * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ ''' point = viewer.getAbsoluteCenterOfView(button) print "Button Absolute Center Position:",point ~~~ ### 4.2 分析和建议 这个API的目的是想定位一个已知ViewNode控件的左上角在屏幕上的绝对坐标。对于我们正常的APP里面的控件,本人实践过是没有问题的。但是有一种情况要特别注意:这个对Menu Options下面的控件是无效的! 以上示例最后一段代码的输出是(3,18),其实这里不用想都知道这个不可能是相对屏幕左上角坐标(0,0)的绝对坐标值了,就偏移这一点点像素,你真的当我的实验机器HTC Incredible S是可以植入脑袋的神器啊。 ![](https://box.kancloud.cn/2016-08-15_57b17557bc133.jpg) 那么这个数据是如何获得的呢?其实按照我的理解(真的只是我自己的理解,不对的话就指正吧,但请描述详细点以供我参考),这个函数的定义应该是“获得从最上层的DecorView(具体DectorView的描述请查看我以前转载的一篇文章《[Android DecorView浅析](http://blog.csdn.net/zhubaitian/article/details/39552069)》)左上角坐标到目标控件的的偏移坐标”,只是这个最上层的DecorView的坐标一般都是从(0,0)开始而已。如下图我认为最上面的那个FrameLayout就代表了DecorView,或者说整个窗体 ![](https://box.kancloud.cn/2016-08-15_57b17557e7eba.jpg) 那么在假设我的观点是对的情况下,这个就很好解析了,请看Menu Option的最上层FrameLayout的绝对坐标是(0,683) ![](https://box.kancloud.cn/2016-08-15_57b175581712e.jpg) 而Add note的绝对坐标是(3,701) ![](https://box.kancloud.cn/2016-08-15_57b1755838d52.jpg) 两者一相减就是和我们的输出结果绝对吻合的(3,18)了。 ## 5. getAbsoluteCenterOfView(ViewNode node) ### 5.1 示例 ~~~ ''' public static Point getAbsoluteCenterOfView(ViewNode node) * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ ''' point = viewer.getAbsoluteCenterOfView(button) print "Button Absolute Center Position:",point ~~~ ### 5.2 分析和建议 这个方法的目的是获得目标ViewNode控件的中间点的绝对坐标值,但是对Menu Options下面的控件同样不适用,具体请查看第3章节。 以下两个方法都不是用来定位控件的,一并记录下来以供参考。 ## 6. getFocusedWindowName() ### 6.1 示例 ~~~ ''' public String getFocusedWindowName() * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. ''' window = viewer.getFocusedWindowName() print "Window Name:",window.encode('utf-8') ~~~ ### 6.2 解析 其实就是获得当前打开的窗口的packageName/activityName,输出与HierarchyViewer工具检测到的信息一致,所以猜想其用到同样的方法。 输出: ![](https://box.kancloud.cn/2016-08-15_57b1755857769.jpg) HierarchyViewer监控信息: ![](https://box.kancloud.cn/2016-08-15_57b175587dd37.jpg) ## 7. visible(ViewNode node) ### 7.1 示例 ~~~ ''' public boolean visible(ViewNode node) * Gets the visibility of a given element. * @param selector selector for the view. * @return True if the element is visible. ''' isVisible = viewer.visible(button) print "is visible:",isVisible ~~~ 就是查看下控件是否可见,没什么好解析的了。 ## 8. 测试代码 ~~~ from com.android.monkeyrunner import MonkeyRunner,MonkeyDevice from com.android.monkeyrunner.easy import EasyMonkeyDevice,By from com.android.chimpchat.hierarchyviewer import HierarchyViewer from com.android.hierarchyviewerlib.models import ViewNode, Window from java.awt import Point #from com.android.hierarchyviewerlib.device import #Connect to the target targetDevice targetDevice = MonkeyRunner.waitForConnection() easy_device = EasyMonkeyDevice(targetDevice) #touch a button by id would need this targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") #time.sleep(2000) #invoke the menu options MonkeyRunner.sleep(6) targetDevice.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); ''' public ViewNode findViewById(String id) * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. ''' viewer = targetDevice.getHierarchyViewer() button = viewer.findViewById('id/title') text = viewer.getText(button) print text.encode('utf-8') ''' public ViewNode findViewById(String id, ViewNode rootNode) * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. ''' iconMenuView = viewer.findViewById('id/icon_menu') button = viewer.findViewById('id/title',iconMenuView) print "Button Text:",text.encode('utf-8') ''' public String getFocusedWindowName() * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. ''' window = viewer.getFocusedWindowName() print "Window Name:",window.encode('utf-8') ''' public static Point getAbsoluteCenterOfView(ViewNode node) * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ ''' point = viewer.getAbsoluteCenterOfView(button) print "Button Absolute Center Position:",point ''' public static Point getAbsolutePositionOfView(ViewNode node) * Gets the absolute x/y position of the view node. * * @param node view node to find position of. * @return point specifying the x/y position of the node. ''' point = viewer.getAbsolutePositionOfView(button) print "Button Absolute Position:", point ''' public boolean visible(ViewNode node) * Gets the visibility of a given element. * @param selector selector for the view. * @return True if the element is visible. ''' isVisible = viewer.visible(button) print "is visible:",isVisible ~~~ ## 9.附上HierarchyViewer类的源码方便参照 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.chimpchat.hierarchyviewer; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.device.DeviceBridge; import com.android.hierarchyviewerlib.device.ViewServerDevice; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import org.eclipse.swt.graphics.Point; /** * Class for querying the view hierarchy of the device. */ public class HierarchyViewer { public static final String TAG = "hierarchyviewer"; private IDevice mDevice; /** * Constructs the hierarchy viewer for the specified device. * * @param device The Android device to connect to. */ public HierarchyViewer(IDevice device) { this.mDevice = device; setupViewServer(); } private void setupViewServer() { DeviceBridge.setupDeviceForward(mDevice); if (!DeviceBridge.isViewServerRunning(mDevice)) { if (!DeviceBridge.startViewServer(mDevice)) { // TODO: Get rid of this delay. try { Thread.sleep(2000); } catch (InterruptedException e) { } if (!DeviceBridge.startViewServer(mDevice)) { Log.e(TAG, "Unable to debug device " + mDevice); throw new RuntimeException("Could not connect to the view server"); } return; } } DeviceBridge.loadViewServerInfo(mDevice); } /** * Find a view by id. * * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. */ public ViewNode findViewById(String id) { ViewNode rootNode = DeviceBridge.loadWindowData( new Window(new ViewServerDevice(mDevice), "", 0xffffffff)); if (rootNode == null) { throw new RuntimeException("Could not dump view"); } return findViewById(id, rootNode); } /** * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. */ public ViewNode findViewById(String id, ViewNode rootNode) { if (rootNode.id.equals(id)) { return rootNode; } for (ViewNode child : rootNode.children) { ViewNode found = findViewById(id,child); if (found != null) { return found; } } return null; } /** * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. */ public String getFocusedWindowName() { int id = DeviceBridge.getFocusedWindow(mDevice); Window[] windows = DeviceBridge.loadWindows(new ViewServerDevice(mDevice), mDevice); for (Window w : windows) { if (w.getHashCode() == id) return w.getTitle(); } return null; } /** * Gets the absolute x/y position of the view node. * * @param node view node to find position of. * @return point specifying the x/y position of the node. */ public static Point getAbsolutePositionOfView(ViewNode node) { int x = node.left; int y = node.top; ViewNode p = node.parent; while (p != null) { x += p.left - p.scrollX; y += p.top - p.scrollY; p = p.parent; } return new Point(x, y); } /** * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ public static Point getAbsoluteCenterOfView(ViewNode node) { Point point = getAbsolutePositionOfView(node); return new Point( point.x + (node.width / 2), point.y + (node.height / 2)); } /** * Gets the visibility of a given element. * * @param selector selector for the view. * @return True if the element is visible. */ public boolean visible(ViewNode node) { boolean ret = (node != null) && node.namedProperties.containsKey("getVisibility()") && "VISIBLE".equalsIgnoreCase( node.namedProperties.get("getVisibility()").value); return ret; } /** * Gets the text of a given element. * * @param selector selector for the view. * @return the text of the given element. */ public String getText(ViewNode node) { if (node == null) { throw new RuntimeException("Node not found"); } ViewNode.Property textProperty = node.namedProperties.get("text:mText"); if (textProperty == null) { // give it another chance, ICS ViewServer returns mText textProperty = node.namedProperties.get("mText"); if (textProperty == null) { throw new RuntimeException("No text property on node"); } } return textProperty.value; } } ~~~ ## 10. 参考阅读 以下是之前不同框架的控件定位的实践,一并列出来方便直接跳转参考: - [Robotium之Android控件定位实践和建议(Appium/UIAutomator姊妹篇)](http://blog.csdn.net/zhubaitian/article/details/39803857) - [UIAutomator定位Android控件的方法实践和建议(Appium姊妹篇)](http://blog.csdn.net/zhubaitian/article/details/39777951) - [Appium基于安卓的各种FindElement的控件定位方法实践和建议](http://blog.csdn.net/zhubaitian/article/details/39754041) <table cellspacing="0" cellpadding="0" width="539" class=" " style="margin:0px 0px 10px; padding:0px; border-collapse:collapse; width:668px; max-width:100%; word-wrap:break-word!important"><tbody style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"> </td></tr><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="111" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">作者</span></p></td><td valign="top" width="112" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">自主博客</span></p></td><td valign="top" width="111" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">微信</span></p></td><td valign="top" width="112" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">CSDN</span></p></td></tr><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="111" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(227,228,228)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">天地会珠海分舵</span></p></td><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; font-size:11px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important"><a target="_blank" href="http://techgogogo.com/">http://techgogogo.com</a></span><span style="margin:0px; padding:0px; max-width:100%; font-family:Helvetica; font-size:11px; letter-spacing:0px; word-wrap:break-word!important"/></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:14px; white-space:pre-wrap; font-family:Helvetica; word-wrap:break-word!important"><br style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"/></p></td><td valign="top" width="111" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">服务号</span><span style="margin:0px; padding:0px; max-width:100%; font-size:10px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">:TechGoGoGo</span></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">扫描码</span><span style="margin:0px; padding:0px; max-width:100%; font-size:10px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">:</span></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:14px; white-space:pre-wrap; font-family:Helvetica; word-wrap:break-word!important"><img src="image/47cf4f9ec59b0ef1f807a6c33ab5ce5f.jpg" alt="" style="max-width:100%; margin:0px; padding:0px; height:auto!important; word-wrap:break-word!important; width:auto!important; visibility:visible!important"/></p></td><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; color:rgb(62,62,62); font-family:'Helvetica Neue',Helvetica,'Hiragino Sans GB','Microsoft YaHei',΢ÈíÑźÚ,Arial,sans-serif; font-size:18px; line-height:28.7999992370605px; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-size:11px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important"><a target="_blank" href="http://blog.csdn.net/zhubaitian">http://blog.csdn.net/zhubaitian</a></span><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-family:Helvetica; font-size:11px; letter-spacing:0px; line-height:28.7999992370605px; word-wrap:break-word!important"/></p><div><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-family:Helvetica; font-size:11px; letter-spacing:0px; line-height:28.7999992370605px; word-wrap:break-word!important"><br/></span></div></td></tr></tbody></table>