在前文<Android工具HierarchyViewer 代碼導讀(3) -- 后台代碼>中,我們講解了HierarchyViewe的后台代碼,指的是HierarchyViewer如何通過ADB和ViewServer這兩個信道和Android設備進行通信,獲取Acitivities信息、控件信息和控件截圖等信息。本文將講解HierarchyViewer的前台代碼,指的是在后台獲取到數據后,HierarchyViewer是如何顯示他們的;當用戶對視圖進行操作時,如選中、放大縮小等,視圖是如何響應的。
MVC模式
前文中我們提到,HierarchyViewer代碼采用的是典型的MVC構架,我們把上文中使用的MVC模式圖再拿出來(這里只討論控件層次圖界面相關的代碼結構):
其中,在TreeViewModel.java文件中定義了ITreeChangeListener接口
public static interface ITreeChangeListener { public void treeChanged(); public void selectionChanged(); public void viewportChanged(); public void zoomChanged(); }
所有的Views – LayoutViewer, TreeViewer, PropertyViewer, TreeViewOverview, TreeViewControllers都實現了該接口。 TreeViewModel維護了一個ITreeChangeListener的ArrayList:
private final ArrayList<ITreeChangeListener> mTreeChangeListeners = new ArrayList<ITreeChangeListener>();
當Views構造時,都會把自己加到mTreeChangeListeners中,當TreeViewModel中的數據改變時,TreeViewModel通過事件通知所有注冊到mTreeCHangeListeners中的Views。
這些事件包括:
treeChanged -- 整個TreeView改變時觸發
selectionChanged -- 選中的節點改變時觸發
viewportChanged -- 當前視見區改變時觸發
zoomChanged -- 當前放大縮小比例改變時觸發
TreeViewModel中保存了四個數據:
private DrawableViewNode mTree; //整個控件樹 private DrawableViewNode mSelectedNode; //當前選中的控件樹 private Rectangle mViewport; //視見區 private double mZoom; //放大縮小比例
Views通過讀取4個數據進繪制或顯示。
TreeView加載
當用戶在主界面雙擊某個Activity,或者在查看控件樹界面點擊刷新時,整個TreeView將重新加載。雙擊或者刷新操作將最終調用HierarchyViewerDirector.java的loadViewTreeData方法:
public void loadViewTreeData(final Window window) { executeInBackground("Loading view hierarchy", new Runnable() { public void run() { mFilterText = ""; //$NON-NLS-1$ ViewNode viewNode = DeviceBridge.loadWindowData(window); if (viewNode != null) { DeviceBridge.loadProfileData(window, viewNode); viewNode.setViewCount(); TreeViewModel.getModel().setData(window, viewNode); } } }); }
這個函數我們在上文中已經提到過,本文主要關心其中2個函數:
DeviceBridge.loadWindowData(window) -- 這個函數做了兩件事情:1)向ViewServer發送DUMP命令,來獲取Acitivity所有控件的信息。 2)獲取到的控件樹信息是文本的形式返回的,如下是其中一個控件的文本信息:
android.widget.FrameLayout@44edba90 mForeground=52,android.graphics.drawable.NinePatchDrawable@44edc1e0 mForegroundInPadding=5,false mForegroundPaddingBottom=1,0 mForegroundPaddingLeft=1,0 mForegroundPaddingRight=1,0 mForegroundPaddingTop=1,0 mMeasureAllChildren=5,false mForegroundGravity=2,55 getDescendantFocusability()=24,FOCUS_BEFORE_DESCENDANTS getPersistentDrawingCache()=9,SCROLLING isAlwaysDrawnWithCacheEnabled()=4,true isAnimationCacheEnabled()=4,true isChildrenDrawingOrderEnabled()=5,false isChildrenDrawnWithCacheEnabled()=5,false mMinWidth=1,0 mPaddingBottom=1,0 mPaddingLeft=1,0 mPaddingRight=1,0 mPaddingTop=2,38 mMinHeight=1,0 mMeasuredWidth=3,480 mMeasuredHeight=3,800 mLeft=1,0 mPrivateFlags_DRAWING_CACHE_INVALID=3,0x0 mPrivateFlags_DRAWN=4,0x20 mPrivateFlags=8,16911408 mID=10,id/content mRight=3,480 mScrollX=1,0 mScrollY=1,0 mTop=1,0 mBottom=3,800 mUserPaddingBottom=1,0 mUserPaddingRight=1,0 mViewFlags=9,402653186 getBaseline()=2,-1 getHeight()=3,800 layout_bottomMargin=1,0 layout_leftMargin=1,0 layout_rightMargin=1,0 layout_topMargin=1,0 layout_height=12,MATCH_PARENT layout_width=12,MATCH_PARENT getTag()=4,null getVisibility()=7,VISIBLE getWidth()=3,480 hasFocus()=5,false isClickable()=5,false isDrawingCacheEnabled()=5,false isEnabled()=4,true isFocusable()=5,false isFocusableInTouchMode()=5,false isFocused()=5,false isHapticFeedbackEnabled()=4,true isInTouchMode()=4,true isOpaque()=5,false isSelected()=5,false isSoundEffectsEnabled()=4,true willNotCacheDrawing()=5,false willNotDraw()=5,false
該文本將被解析,所有信息將保存在ViewNode對象中。文本中所有的屬性都同時保存在ViewNode的List<Property> properties和Map<String, Property> namedProperties中,一些和繪制視圖相關的屬性,如top,paddingLeft,marginBottom等等,除了保存在properties和namedProperties中,還將直接保存在ViewNode的成員變量中。
ViewNode是一個樹,每個ViewNode節點中保存了它的父節點和子節點。文本解析的時候,是如何確定ViewNode父節點的呢?原來每行文本信息前面都有若干個空格,空格的數量決定了這個節點的深度,如5個空格表示這個節點在第6層,它的父節點就是最近收到的,有4個空格的節點。具體解析過程大家可以輸入閱讀loadWindowData函數。
TreeViewModel.getModel().setData(window, viewNode) -- 更新TreeViewModel的TreeView
讓我們step into TreeViewModel.getModel().setData(window, viewNode)函數:
public void setData(Window window, ViewNode viewNode) { synchronized (this) { if (mTree != null) { mTree.viewNode.dispose(); } this.mWindow = window; if (viewNode == null) { mTree = null; } else { mTree = new DrawableViewNode(viewNode); mTree.setLeft(); mTree.placeRoot(); } mViewport = null; mZoom = 1; mSelectedNode = null; } notifyTreeChanged(); }
以上函數中:
mTree = new DrawableViewNode(viewNode) –通過ViewNode樹來構造DrawableViewNode樹。為什么已經有了ViewNode結構還要再構造一個DrawableViewNode結構呢? 它們的功能是不同的,ViewNode是面向數據的,它對應的是Acitivity中每個控件節點的信息; 而DrawableViewNode面向的是圖形繪制,它通過計算ViewNode中提供的數據,確定如何在Hierarchy view中進行繪制。讀者深入閱讀該構造函數,它的作用是根據ViewNode來遞歸地構造整個DrawableViewNode控件樹,並根據每個子樹的size確定每個子樹在Hierarchy view繪制時中占據的高度。
mTree.setLeft() -- 計算樹中每個節點在Hierarchy view繪制時的left值。
mTree.placeRoot() -- 計算樹中每個節點在Hierarchy view繪制時的top值。
mViewport = null,mZoom = 1,mSelectedNode = null -- 初始化視見區,放大縮小比例和當前選中節點。
notifyTreeChanged() -- 觸發treeChanged事件。
最后,TreeViewOverview.java, LayoutViewer, TreeViewer都是通過響應treeChanged事件,並最終調用PaintListener事件,根據TreeViewModel中的mTree,mViewport,mZoom,mSelectedNode的數據來繪制圖形的(這3個類都是繼承Canvas類)。
這3個類中的PaintListener事件中圖形繪制的代碼都很值得一讀,但本文限於篇幅不能詳細介紹了。
用戶事件響應
當用戶在一個View中進行操作,其他View也會響應這個操作。如在TreeView中滾動滾輪,TreeViewOverview也會跟着放大縮小;在LayoutViewer中選中某個節點,TreeView和TreeViewOverview中也會跟着選中,這一切是怎么發生的呢?
通過上一節,其實我們很容易理解HierarchyViewer是怎么做的了,這還是一個經典的MVC模式的例子:TreeViewModel提供了如下公開方法(加上上節中的setData方法,一共4個方法)來改變TreeViewModel中的數據:
public void setSelection(DrawableViewNode selectedNode) public void setViewport(Rectangle viewport) public void setZoom(double newZoom)
當在某View中選中節點時,移動視見區,放大縮小時,View將調用對應的方法來修改TreeViewModel中的數據,然后對應的事件 -- selectionChanged,viewportChanged和zoomChanged將被觸發,Views通過響應這些事件,在PaintListener中重繪圖形。這是一個用戶操作View,View調用Model,Model觸發事件,Views響應事件的過程。
Note:
1)不是所有的Views都關心所有的事件。如LayoutViewer不關心zoomChanged和viewportChanged事件;PropertyViewer只關心selectionChanged事件。
2)用戶選中一個節點時,需要進行坐標轉換,遍歷所有的點才能找到選中的節點;在LayoutViewer中,需要找到的是符合條件的,層次低的節點。
本系列到此結束。我相信閱讀HierarchyViewer和其他一些sdk工具的源代碼,對於理解Android的機制是有幫助的。同時,對於學習MVC也會助益不少,google工程師的代碼的確很簡潔優秀。
知平軟件致力於移動平台自動化測試技術的研究,我們希望通過向社區貢獻知識和開源項目,來促進行業和自身的發展。