欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

从源码解析Android中View的容器ViewGroup

程序员文章站 2024-02-29 10:28:46
 这回我们是深入到viewgroup内部\,了解viewgroup的工作,同时会阐述更多有关于view的相关知识。以便为以后能灵活的使用自定义空间打更近一步的基础...

 这回我们是深入到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());  
    }  
} 

效果图片:

从源码解析Android中View的容器ViewGroup

七、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的层次结构,如下图

从源码解析Android中View的容器ViewGroup

我们看到最顶层就是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()方法)

从源码解析Android中View的容器ViewGroup