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

从源码解析Android中View的容器ViewGroup,第1张

概述 这回我们是深入到ViewGroup内部\\,了解ViewGroup的工作,同时会阐述更多有关于View的相关知识。以便为以后能灵活的使用自定义空间打更近一步的基础。希望有志同道合的朋友一起来探讨,深入Android内部,深入理

 这回我们是深入到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,LayoutParams params,boolean preventRequestLayout) {      child.mParent = null;      addVIEwInner(child,preventRequestLayout);      child.mPrivateFlags = (child.mPrivateFlags & ~DIRTY_MASK) | DRAWN;      return true;    }  private voID addVIEwInner(VIEw child,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,mChildren,index);  System.arraycopy(children,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)测量指定的子组件,为子组件添加padding和margin。   

  protected voID measureChilDWithmargins(VIEw child,int wIDthUsed,int parentHeightmeasureSpec,int heightUsed) {      final marginLayoutParams lp = (marginLayoutParams) child.getLayoutParams();         final int chilDWIDthMeasureSpec = getChildMeasureSpec(parentWIDthMeasureSpec,mpaddingleft + mpaddingRight + lp.leftmargin + lp.rightmargin              + wIDthUsed,mpaddingtop + mpaddingBottom + lp.topmargin + lp.bottommargin              + heightUsed,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 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,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 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()方法)


总结

以上是内存溢出为你收集整理的从源码解析Android中View的容器ViewGroup全部内容,希望文章能够帮你解决从源码解析Android中View的容器ViewGroup所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/1141219.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-31
下一篇2022-05-31

发表评论

登录后才能评论

评论列表(0条)

    保存