从源码解析Android中View的容器ViewGroup
这回我们是深入到viewgroup内部\,了解viewgroup的工作,同时会阐述更多有关于view的相关知识。以便为以后能灵活的使用自定义空间打更近一步的基础。希望有志同道合的朋友一起来探讨,深入android内部,深入理解android。
一、viewgroup是什么?
一个viewgroup是一个可以包含子view的容器,是布局文件和view容器的基类。在这个类里定义了viewgroup.layoutparams类,这个类是布局参数的子类。
其实viewgroup也就是view的容器。通过viewgroup.layoutparams来指定子view的参数。
viewgroup作为一个容器,为了制定这个容器应有的标准所以为其指定了接口
public abstract class viewgroup extends view implements viewparent, viewmanager
这两个接口这里不研究,如果涉及到的话会带一下。viewgroup有小4000行代码,下面我们一个模块一个模块分析。
二、viewgroup这个容器
viewgroup是一个容器,其采用一个数组来存储这些子view:
// child views of this viewgroup private view[] mchildren;
由于是通过一个数组来存储view数据的,所以对于viewgroup来说其必须实现增、删、查的算法。下面我们就来看看其内部实现。
2.1 添加view的算法
protected boolean addviewinlayout(view child, int index, layoutparams params) { return addviewinlayout(child, index, params, false); } protected boolean addviewinlayout(view child, int index, layoutparams params, boolean preventrequestlayout) { child.mparent = null; addviewinner(child, index, params, preventrequestlayout); child.mprivateflags = (child.mprivateflags & ~dirty_mask) | drawn; return true; } private void addviewinner(view child, int index, layoutparams params, boolean preventrequestlayout) { ... addinarray(child, index); ... } private void addinarray(view child, int index) { ... }
上面四个方法就是添加view的核心算法的封装,它们是层层调用的关系。而我们通常调用的addview就是最终通过上面那个来最终达到添加到viewgroup中的。
2.1.1 我们先来分析addviewinner方法:
首先是对子view是否已经包含到一个父容器中,主要的防止添加一个已经有父容器的view,因为添加一个拥有父容器的view时会碰到各种问题。比如记录本身父容器算法的问题、本身被多个父容器包含时更新的处理等等一系列的问题都会出现。
if (child.getparent() != null) { throw new illegalstateexception("the specified child already has a parent. " + "you must call removeview() on the child's parent first."); }
然后就是对子view布局参数的处理。
调用addinarray来添加view
父view为当前的viewgroup
焦点的处理。
当前view的attachinfo信息,这个信息是用来在窗口处理中用的。android的窗口系统就是用过attachinfo来判断view的所属窗口的,这个了解下就行。详细信息设计到android框架层的一些东西。
attachinfo ai = mattachinfo; if (ai != null) { boolean lastkeepon = ai.mkeepscreenon; ai.mkeepscreenon = false; child.dispatchattachedtowindow(mattachinfo, (mviewflags&visibility_mask)); if (ai.mkeepscreenon) { needglobalattributesupdate(true); } ai.mkeepscreenon = lastkeepon; }
view树改变的监听
if (monhierarchychangelistener != null) { monhierarchychangelistener.onchildviewadded(this, child); }
子view中的mviewflags的设置:
if ((child.mviewflags & duplicate_parent_state) == duplicate_parent_state) { mgroupflags |= flag_notify_children_on_drawable_state_change; }
2.1.2 addinarray
这个里面的实现主要是有个知识点,以前也没用过arraycopy,这里具体实现就不多加描述了。
system.arraycopy(children, 0, mchildren, 0, index); system.arraycopy(children, index, mchildren, index + 1, count - index);
2.2 移除view
移除view的几种方式:
(1)移除指定的view。
(2)移除从指定位置的view
(3)移除从指定位置开始的多个view
(4)移除所有的view
其中具体涉及到的方法就有好多了,不过最终对要删除的子view中所做的无非就是下列的事情:
如果拥有焦点则清楚焦点
将要删除的view从当前的window中解除关系。
设置view树改变的事件监听,我们可以通过监听onhierarchychangelistener事件来进行一些相应的处理。
从父容器的子容器数组中删除。
具体的内容这里就不一一贴出来了,大家回头看看源码就哦了。
2.3 查询
这个就简单了,就是直接从数组中取出就可以了:
public view getchildat(int index) { try { return mchildren[index]; } catch (indexoutofboundsexception ex) { return null; } }
分析到这儿,其实我们已经相当于分析了viewgroup四分之一的代码了,呵呵。
三、onfinishinflate
我们一般使用view的流程是在oncreate中使用setcontentview来设置要显示layout文件或直接创建一个view,在当设置了contentview之后系统会对这个view进行解析,然后回调当前视图view中的onfinishinflate方法。只有解析了这个view我们才能在这个view容器中获取到拥有id的组件,同样因为系统解析完view之后才会调用onfinishinflate方法,所以我们自定义组件时可以onfinishinflate方法中获取指定子view的引用。
四、测量组件
在viewgroup中提供了测量子组件的三个方法。
1、measurechild(view, int, int),为子组件添加padding
protected void measurechild(view child, int parentwidthmeasurespec, int parentheightmeasurespec) { final layoutparams lp = child.getlayoutparams(); final int childwidthmeasurespec = getchildmeasurespec(parentwidthmeasurespec, mpaddingleft + mpaddingright, lp.width); final int childheightmeasurespec = getchildmeasurespec(parentheightmeasurespec, mpaddingtop + mpaddingbottom, lp.height); child.measure(childwidthmeasurespec, childheightmeasurespec); }
2、measurechildren(int, int)根据指定的高和宽来测量所有子view中显示参数非gone的组件。
protected void measurechildren(int widthmeasurespec, int heightmeasurespec) { final int size = mchildrencount; final view[] children = mchildren; for (int i = 0; i < size; ++i) { final view child = children[i]; if ((child.mviewflags & visibility_mask) != gone) { measurechild(child, widthmeasurespec, heightmeasurespec); } } }
3、measurechildwithmargins(view, int, int, int, int)测量指定的子组件,为子组件添加padding和margin。
protected void measurechildwithmargins(view child, int parentwidthmeasurespec, int widthused, int parentheightmeasurespec, int heightused) { final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams(); final int childwidthmeasurespec = getchildmeasurespec(parentwidthmeasurespec, mpaddingleft + mpaddingright + lp.leftmargin + lp.rightmargin + widthused, lp.width); final int childheightmeasurespec = getchildmeasurespec(parentheightmeasurespec, mpaddingtop + mpaddingbottom + lp.topmargin + lp.bottommargin + heightused, lp.height); child.measure(childwidthmeasurespec, childheightmeasurespec); }
上面三个方法都是为子组件设置了布局参数。最终调用的方法是子组件的measure方法。在view中我们知道这个调用实际上就是设置了子组件的布局参数并且调用onmeasure方法,最终设置了view测量后的高度和宽度。
五、onlayout
这个函数是一个抽象函数,要求实现viewgroup的函数必须实现这个函数,这也就是viewgroup是一个抽象函数的原因。因为各种组件实现的布局方式不一样,而onlayout是必须被重载的函数。
@override protected abstract void onlayout(boolean changed, int l, int t, int r, int b);
来看view中layout方法:
public final void layout(int l, int t, int r, int b) { boolean changed = setframe(l, t, r, b); if (changed || (mprivateflags & layout_required) == layout_required) { if (viewdebug.trace_hierarchy) { viewdebug.trace(this, viewdebug.hierarchytracetype.on_layout); } onlayout(changed, l, t, r, b); mprivateflags &= ~layout_required; } mprivateflags &= ~force_layout; }
在这个方法中调用了setframe方法,这个方法是用来设置view中的上下左右边距用的
protected boolean setframe(int left, int top, int right, int bottom) { boolean changed = false; //....... if (mleft != left || mright != right || mtop != top || mbottom != bottom) { changed = true; // remember our drawn bit int drawn = mprivateflags & drawn; // invalidate our old position invalidate(); int oldwidth = mright - mleft; int oldheight = mbottom - mtop; mleft = left; mtop = top; mright = right; mbottom = bottom; mprivateflags |= has_bounds; int newwidth = right - left; int newheight = bottom - top; if (newwidth != oldwidth || newheight != oldheight) { onsizechanged(newwidth, newheight, oldwidth, oldheight); } if ((mviewflags & visibility_mask) == visible) { // if we are visible, force the drawn bit to on so that // this invalidate will go through (at least to our parent). // this is because someone may have invalidated this view // before this call to setframe came in, therby clearing // the drawn bit. mprivateflags |= drawn; invalidate(); } // reset drawn bit to original value (invalidate turns it off) mprivateflags |= drawn; mbackgroundsizechanged = true; } return changed; }
我们可以看到如果新的高度和宽度改变之后会调用重新设置view的四个参数:
(1)protected int mleft;
(2)protected int mright;
(3)protected int mtop;
(4)protected int mbottom;
这四个参数指定了view将要布局的位置。而绘制的时候是通过这四个参数来绘制,所以我们在view中调用layout方法可以实现指定子view中布局。
六、viewgroup的绘制。
viewgroup的绘制实际上是调用的dispatchdraw,绘制时需要考虑动画问题,而动画的实现实际上就通过dispatchdraw来实现的。
我们不用理会太多的细节,直接看其绘制子组件调用的是drawchild方法,这个里面具体的东西就多了,涉及到动画效果的处理,如果有机会的话再写,我们只要知道这个方法的功能就行。
这里有个demo贴出其中的代码大家可以测试下。
public viewgroup01(context context) { super(context); button mbutton = new button(context); mbutton.settext("测试"); addview(mbutton); } @override protected void onlayout(boolean changed, int l, int t, int r, int b) { view v = getchildat(0); if(v != null) { v.layout(120, 120, 250, 250); } } @override protected void dispatchdraw(canvas canvas) { super.dispatchdraw(canvas); view v = getchildat(0); if(v != null) { drawchild(canvas, v, getdrawingtime()); } }
效果图片:
七、viewgroup的事件分发机制
我们用手指去触摸android手机屏幕,就会产生一个触摸事件,但是这个触摸事件在底层是怎么分发的呢?这个我还真不知道,这里涉及到操作硬件(手机屏幕)方面的知识,也就是linux内核方面的知识,我也没有了解过这方面的东西,所以我们可能就往上层来分析分析,我们知道android中负责与用户交互,与用户操作紧密相关的四大组件之一是activity, 所以我们有理由相信activity中存在分发事件的方法,这个方法就是dispatchtouchevent(),我们先看其源码吧
public boolean dispatchtouchevent(motionevent ev) { //如果是按下状态就调用onuserinteraction()方法,onuserinteraction()方法 //是个空的方法, 我们直接跳过这里看下面的实现 if (ev.getaction() == motionevent.action_down) { onuserinteraction(); } if (getwindow().superdispatchtouchevent(ev)) { return true; } //getwindow().superdispatchtouchevent(ev)返回false,这个事件就交给activity //来处理, activity的ontouchevent()方法直接返回了false return ontouchevent(ev); }
这个方法中我们还是比较关心getwindow()的superdispatchtouchevent()方法,getwindow()返回当前activity的顶层窗口window对象,我们直接看window api的superdispatchtouchevent()方法
/** * used by custom windows, such as dialog, to pass the touch screen event * further down the view hierarchy. application developers should * not need to implement or call this. * */ public abstract boolean superdispatchtouchevent(motionevent event);
这个是个抽象方法,所以我们直接找到其子类来看看superdispatchtouchevent()方法的具体逻辑实现,window的唯一子类是phonewindow,我们就看看phonewindow的superdispatchtouchevent()方法
public boolean superdispatchtouchevent(keyevent event) { return mdecor.superdispatctouchevent(event); }
里面直接调用decorview类的superdispatchtouchevent()方法,或许很多人不了解decorview这个类,decorview是phonewindow的一个final的内部类并且继承framelayout的,也是window界面的最顶层的view对象,这是什么意思呢?别着急,我们接着往下看
我们先新建一个项目,取名androidtouchevent,然后直接用模拟器运行项目, mainactivity的布局文件为
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".mainactivity" > <textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerhorizontal="true" android:layout_centervertical="true" android:text="@string/hello_world" /> </relativelayout>
利用hierarchyviewer工具来查看下mainactivity的view的层次结构,如下图
我们看到最顶层就是phonewindow$decorview,接着decorview下面有一个linearlayout, linearlayout下面有两个framelayout
上面那个framelayout是用来显示标题栏的,这个demo中是一个textview,当然我们还可以定制我们的标题栏,利用getwindow().setfeatureint(window.feature_custom_title,r.layout.xxx); xxx就是我们自定义标题栏的布局xml文件
下面的framelayout是用来装载contentview的,也就是我们在activity中利用setcontentview()方法设置的view,现在我们知道了,原来我们利用setcontentview()设置activity的view的外面还嵌套了这么多的东西
我们来理清下思路,activity的最顶层窗体是phonewindow,而phonewindow的最顶层view是decorview,接下来我们就看decorview类的superdispatchtouchevent()方法
public boolean superdispatchtouchevent(motionevent event) { return super.dispatchtouchevent(event); }
在里面调用了父类framelayout的dispatchtouchevent()方法,而framelayout中并没有dispatchtouchevent()方法,所以我们直接看viewgroup的dispatchtouchevent()方法
/** * {@inheritdoc} */ @override public boolean dispatchtouchevent(motionevent ev) { final int action = ev.getaction(); final float xf = ev.getx(); final float yf = ev.gety(); final float scrolledxfloat = xf + mscrollx; final float scrolledyfloat = yf + mscrolly; final rect frame = mtemprect; //这个值默认是false, 然后我们可以通过requestdisallowintercepttouchevent(boolean disallowintercept)方法 //来改变disallowintercept的值 boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0; //这里是action_down的处理逻辑 if (action == motionevent.action_down) { //清除mmotiontarget, 每次action_down都很设置mmotiontarget为null if (mmotiontarget != null) { mmotiontarget = null; } //disallowintercept默认是false, 就看viewgroup的onintercepttouchevent()方法 if (disallowintercept || !onintercepttouchevent(ev)) { ev.setaction(motionevent.action_down); final int scrolledxint = (int) scrolledxfloat; final int scrolledyint = (int) scrolledyfloat; final view[] children = mchildren; final int count = mchildrencount; //遍历其子view for (int i = count - 1; i >= 0; i--) { final view child = children[i]; //如果该子view是visible或者该子view正在执行动画, 表示该view才 //可以接受到touch事件 if ((child.mviewflags & visibility_mask) == visible || child.getanimation() != null) { //获取子view的位置范围 child.gethitrect(frame); //如touch到屏幕上的点在该子view上面 if (frame.contains(scrolledxint, scrolledyint)) { // offset the event to the view's coordinate system final float xc = scrolledxfloat - child.mleft; final float yc = scrolledyfloat - child.mtop; ev.setlocation(xc, yc); child.mprivateflags &= ~cancel_next_up_event; //调用该子view的dispatchtouchevent()方法 if (child.dispatchtouchevent(ev)) { // 如果child.dispatchtouchevent(ev)返回true表示 //该事件被消费了,设置mmotiontarget为该子view mmotiontarget = child; //直接返回true return true; } // the event didn't get handled, try the next view. // don't reset the event's location, it's not // necessary here. } } } } } //判断是否为action_up或者action_cancel boolean isuporcancel = (action == motionevent.action_up) || (action == motionevent.action_cancel); if (isuporcancel) { //如果是action_up或者action_cancel, 将disallowintercept设置为默认的false //假如我们调用了requestdisallowintercepttouchevent()方法来设置disallowintercept为true //当我们抬起手指或者取消touch事件的时候要将disallowintercept重置为false //所以说上面的disallowintercept默认在我们每次action_down的时候都是false mgroupflags &= ~flag_disallow_intercept; } // the event wasn't an action_down, dispatch it to our target if // we have one. final view target = mmotiontarget; //mmotiontarget为null意味着没有找到消费touch事件的view, 所以我们需要调用viewgroup父类的 //dispatchtouchevent()方法,也就是view的dispatchtouchevent()方法 if (target == null) { // we don't have a target, this means we're handling the // event as a regular view. ev.setlocation(xf, yf); if ((mprivateflags & cancel_next_up_event) != 0) { ev.setaction(motionevent.action_cancel); mprivateflags &= ~cancel_next_up_event; } return super.dispatchtouchevent(ev); } //这个if里面的代码action_down不会执行,只有action_move //action_up才会走到这里, 假如在action_move或者action_up拦截的 //touch事件, 将action_cancel派发给target,然后直接返回true //表示消费了此touch事件 if (!disallowintercept && onintercepttouchevent(ev)) { final float xc = scrolledxfloat - (float) target.mleft; final float yc = scrolledyfloat - (float) target.mtop; mprivateflags &= ~cancel_next_up_event; ev.setaction(motionevent.action_cancel); ev.setlocation(xc, yc); if (!target.dispatchtouchevent(ev)) { } // clear the target mmotiontarget = null; // don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal ontouchevent(). return true; } if (isuporcancel) { mmotiontarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledxfloat - (float) target.mleft; final float yc = scrolledyfloat - (float) target.mtop; ev.setlocation(xc, yc); if ((target.mprivateflags & cancel_next_up_event) != 0) { ev.setaction(motionevent.action_cancel); target.mprivateflags &= ~cancel_next_up_event; mmotiontarget = null; } //如果没有拦截action_move, action_down的话,直接将touch事件派发给target return target.dispatchtouchevent(ev); }
这个方法相对来说还是蛮长,不过所有的逻辑都写在一起,看起来比较方便,接下来我们就具体来分析一下
我们点击屏幕上面的textview来看看touch是如何分发的,先看看action_down
在decorview这一层会直接调用viewgroup的dispatchtouchevent(), 先看18行,每次action_down都会将mmotiontarget设置为null, mmotiontarget是什么?我们先不管,继续看代码,走到25行, disallowintercept默认为false,我们再看viewgroup的onintercepttouchevent()方法
public boolean onintercepttouchevent(motionevent ev) { return false; }
直接返回false, 继续往下看,循环遍历decorview里面的child,从上面的mainactivity的层次结构图我们可以看出,decorview里面只有一个child那就是linearlayout, 第43行判断touch的位置在不在linnearlayout上面,这是毫无疑问的,所以直接跳到51行, 调用linearlayout的dispatchtouchevent()方法,linearlayout也没有dispatchtouchevent()这个方法,所以也是调用viewgroup的dispatchtouchevent()方法,所以这个方法卡在51行没有继续下去,而是去先执行linearlayout的dispatchtouchevent()
linearlayout调用dispatchtouchevent()的逻辑跟decorview是一样的,所以也是遍历linearlayout的两个framelayout,判断touch的是哪个framelayout,很明显是下面那个,调用下面那个framelayout的dispatchtouchevent(), 所以linearlayout的dispatchtouchevent()卡在51也没继续下去
继续调用framelayout的dispatchtouchevent()方法,和上面一样的逻辑,下面的framelayout也只有一个child,就是relativelayout,framelayout的dispatchtouchevent()继续卡在51行,先执行relativelayout的dispatchtouchevent()方法
执行relativelayout的dispatchtouchevent()方法逻辑还是一样的,循环遍历 relativelayout里面的孩子,里面只有一个textview, 所以这里就调用textview的dispatchtouchevent(), textview并没有dispatchtouchevent()这个方法,于是找textview的父类view,在看view的dispatchtouchevent()的方法之前,我们先理清下上面这些viewgroup执行dispatchtouchevent()的思路,我画了一张图帮大家理清下(这里没有画出onintercepttouchevent()方法)
上一篇: 浅谈java中的局部变量和全局变量
下一篇: 基于spring 方法级缓存的多种实现
推荐阅读
-
从源码解析Android中View的容器ViewGroup
-
Android应用开发中自定义ViewGroup视图容器的教程
-
Android AsyncTask完全解析 带你从源码的角度彻底理解
-
解析Android中View转换为Bitmap及getDrawingCache=null的解决方法
-
Android 中 SwipeLayout一个展示条目底层菜单的侧滑控件源码解析
-
Android应用开发中View绘制的一些优化点解析
-
从源码剖析Android中的Intent组件
-
从源码解析Android中View的容器ViewGroup
-
Android 中 SwipeLayout一个展示条目底层菜单的侧滑控件源码解析
-
Android AsyncTask完全解析 带你从源码的角度彻底理解