## 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
所以在這里嘗試整理下HierarchyViewer提供的API的用法并根據實踐作出相應的建議,首先請看該類提供的所有可用的公共方法,內容并不多:

從圖中可以看出HierarchyViewer類中提供的方法主要是用來定位控件相關的,包括根據ID取得控件,根據控件取得控件在屏幕的位置等。但還有一些其他方法,我們會順帶一并描述,畢竟內容并不多。
本文我們依然跟上幾篇文章一樣以SDK自帶的NotePad為實驗目標,看怎么定位到NotesList下面的Menu Options中的Add note這個Menu Entry。
以下是通過HierarchyViewer這個工具獲得的目標設備界面的截圖:
## 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:"字串:
- 這個方法返回的一個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是可以植入腦袋的神器啊。

那么這個數據是如何獲得的呢?其實按照我的理解(真的只是我自己的理解,不對的話就指正吧,但請描述詳細點以供我參考),這個函數的定義應該是“獲得從最上層的DecorView(具體DectorView的描述請查看我以前轉載的一篇文章《[Android DecorView淺析](http://blog.csdn.net/zhubaitian/article/details/39552069)》)左上角坐標到目標控件的的偏移坐標”,只是這個最上層的DecorView的坐標一般都是從(0,0)開始而已。如下圖我認為最上面的那個FrameLayout就代表了DecorView,或者說整個窗體

那么在假設我的觀點是對的情況下,這個就很好解析了,請看Menu Option的最上層FrameLayout的絕對坐標是(0,683)

而Add note的絕對坐標是(3,701)

兩者一相減就是和我們的輸出結果絕對吻合的(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工具檢測到的信息一致,所以猜想其用到同樣的方法。
輸出:

HierarchyViewer監控信息:

## 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',?¢èí??oú,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>
- 前言
- MonkeyRunner創建一個Note的實例
- MonkeyRunner在Windows下的Eclipse開發環境搭建步驟(兼解決網上Jython配置出錯的問題)
- MonkenRunner通過HierarchyViewer定位控件的方法和建議(Appium/UIAutomator/Robotium姊妹篇)
- MonkeyDevcie API 實踐全記錄
- MonkeyImage API 實踐全記錄
- EasyMonkeyDevice vs MonkeyDevice&amp;HierarchyViewer API Mapping Matrix
- adb概覽及協議參考
- MonkeyRunner源碼分析之-誰動了我的截圖?
- MonkeyRunner源碼分析之與Android設備通訊方式
- MonkeyRunner源碼分析之啟動
- Monkey源碼分析之運行流程
- Monkey源碼分析之事件源
- Monkey源碼分析番外篇之WindowManager注入事件如何跳出進程間安全限制
- Monkey源碼分析番外篇之Android注入事件的三種方法比較
- Monkey源碼分析之事件注入
- monkey源碼分析之事件注入方法變化
- MonkeyRunner源碼分析之工作原理圖
- Android自動化測試框架新書:&lt;&lt;MonnkeyRunner實現原理剖析&gt;&gt;交流