Android工具HierarchyViewer代码导读(4) -- 前台代码

本文深入探讨了HierarchyViewer的前端实现细节,包括MVC架构的应用、控件树加载流程及用户交互事件处理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前文<Android工具HierarchyViewer 代码导读(3) -- 后台代码>中,我们讲解了HierarchyViewe的后台代码,指的是HierarchyViewer如何通过ADB和ViewServer这两个信道和Android设备进行通信,获取Acitivities信息、控件信息和控件截图等信息。本文将讲解HierarchyViewer的前台代码,指的是在后台获取到数据后,HierarchyViewer是如何显示他们的;当用户对视图进行操作时,如选中、放大缩小等,视图是如何响应的。

 

MVC模式

前文中我们提到,HierarchyViewer代码采用的是典型的MVC构架,我们把上文中使用的MVC模式图再拿出来(这里只讨论控件层次图界面相关的代码结构):

image

 

 

其中,在TreeViewModel.java文件中定义了ITreeChangeListener接口

?
1
2
3
4
5
6
7
8
9
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:

?
1
2
private final ArrayList<ITreeChangeListener> mTreeChangeListeners =
         new ArrayList<ITreeChangeListener>();

 

当Views构造时,都会把自己加到mTreeChangeListeners中,当TreeViewModel中的数据改变时,TreeViewModel通过事件通知所有注册到mTreeCHangeListeners中的Views。

 

这些事件包括:

treeChanged -- 整个TreeView改变时触发

selectionChanged -- 选中的节点改变时触发

viewportChanged -- 当前视见区改变时触发

zoomChanged -- 当前放大缩小比例改变时触发

 

TreeViewModel中保存了四个数据:

?
1
2
3
4
5
6
7
private DrawableViewNode mTree; //整个控件树
 
private DrawableViewNode mSelectedNode; //当前选中的控件树
 
private Rectangle mViewport; //视见区
 
private double mZoom;  //放大缩小比例

Views通过读取4个数据进绘制或显示。

 

TreeView加载

当用户在主界面双击某个Activity,或者在查看控件树界面点击刷新时,整个TreeView将重新加载。双击或者刷新操作将最终调用HierarchyViewerDirector.java的loadViewTreeData方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)获取到的控件树信息是文本的形式返回的,如下是其中一个控件的文本信息:

?
1
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)函数:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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中的数据:

?
1
2
3
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工程师的代码的确很简洁优秀。

 

本文由知平软件刘斌华原创,转载请注明出处。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值