標籤:
1. 背景
在使用MonkeyRunner的時候我們經常會用到Chimchat下面的HierarchyViewer模組來擷取目標控制項的一些資訊來輔助我們測試,但在MonkeyRunner的官網上是沒有看到相應的API的描述的,上面只有以下三個類的API引用資訊(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
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的描述請查看我以前CSDN轉載的一篇文章《Android DecorView淺析》)左上方座標到目標控制項的的位移座標”,只是這個最上層的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,MonkeyDevicefrom com.android.monkeyrunner.easy import EasyMonkeyDevice,Byfrom com.android.chimpchat.hierarchyviewer import HierarchyViewerfrom com.android.hierarchyviewerlib.models import ViewNode, Windowfrom java.awt import Point#from com.android.hierarchyviewerlib.device import #Connect to the target targetDevicetargetDevice = MonkeyRunner.waitForConnection()easy_device = EasyMonkeyDevice(targetDevice) #touch a button by id would need thistargetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList")#time.sleep(2000)#invoke the menu optionsMonkeyRunner.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:",isVisible9.附上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姊妹篇)
- UIAutomator定位Android控制項的方法實踐和建議(Appium姊妹篇)
- Appium基於安卓的各種FindElement的控制項定位方法實踐和建議
| 作者 |
自主部落格 |
服務號及掃描碼 |
CSDN |
| 天地會珠海分舵 |
http://techgogogo.com |
服務號:TechGoGoGo掃描碼: |
http://blog.csdn.net/zhubaitian |
MonkenRunner通過HierarchyViewer定位控制項的方法和建議(Appium/UIAutomator/Robotium姊妹篇)