自定义容器

  容器类一般要继承ViewGroup类,ViewGroup类同时也是View的子类。ViewGroup又是一个抽象类,定义了onLayout()等抽象方法。当然,根据需要,我们也可以让容器类继承自FrameLayout等ViewGroup的子类。比如ListView继承自ViewGroup,而ScrollView继承自FrameLayout。

ViewGroup类

ViewGroup常用方法

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    private View[] mChildren;
    private int mChildrenCount;
    
    public int getChildCount(){}
    
    public View getChildAt(int index){}
    
    public void addView(View child){}
    
    public void addView(View child,int index){}
    
    public void addView(View child,int width,int height){}
    
    public void addView(View child,LayoutParams params){}
    
    public void addView(View child,int index,LayoutParams params){}
    
    
}

  在ViewGroup中,定义了一个View[]类型的数组mChildren,该数组保存了容器中所有的子组件,负责维护组件的添加、移除、管理组件顺序等功能,另一个成员变量mChildrenCount则保存了容器中子组件的数量。在layout布局文件中,容器中的子元素会根据顺序自动添加到mChildren数组中。

  ViewGroup具备了容器类的基本特征和运作流程,也定义了相关的方法用于访问容器内的组件,主要的方法有:

//获取容器内的子组件的个数;
public int getChildCount()

//容器内的所有子组件都存储在名为mChildren的View[]数组中,该方法通过索引index找到指定位置的子组件;
public View getChildAt(int index)

//向容器中添加新的子组件,child表示子组件(也可以是子容器),index表示索引,指定组件所在的位置,params参数为组件指定布局参数。该方法还有两个简化的版本;
public void addView(View child,int index,LayoutParams params)

//添加child子组件,并为该子组件指定布局参数;
public void addView(View child,LayoutParams params)

//布局参数使用默认的ViewGroup.LayoutParams,其中layout_width和layout_height均为wrap_content;
public void addView(View child,int index)

//布局参数同上,但index为-1,表示将child组件添加到mChildren数组的最后。向容器中添加新的子组件时,子组件不能有父容器,否则会报错;
public void addView(View child)

//移除index位置的子组件;
public void removeViewAt(int index)

//移除子组件View;
pubic void removeView(View view)

//移除从start开始连续的count个子组件;
public void removeViews(int start,int count)

//测量子组件的尺寸;
protected void measureChild(View child,int parentWidthMeasureSpec,int parentHeightMeasureSpec)

//测量所有子组件的尺寸;
protected void measureChildren(int parentWidthMeasureSpec,int parentHeightMeasureSpec)

//该方法从View类中继承,用于测量组件或容器自己的尺寸,参数 widthMeasureSpec 和 heightMeasureSpec 为 0 时表示按实际大小进行测量;
public final void measure(int widthMeasureSpec,int heightMeasureSpec)

  ViewGroup运行的基本流程大致为:
1 . 测量容器尺寸:
  重写onMeasure()方法测量容器的大小,和自定义组件有所区别的是,在测量容器大小之前,必须先调用measureChildren()方法测量所有子组件的大小,不然结果永远为0。

2 . 确定每个子组件的位置:
  重写onLayout()方法确定每个子组件的位置,在onLayout()方法中,调用View的layout()方法确定子组件的位置。

3 . 绘制容器
  重写onDraw()方法,其实ViewGroup类并没有重写onDraw()方法,除非有特别的要求,自定义容器很少去重写。比如LinearLayout重写了该方法用于绘制水平或垂直分割线,而FrameLayout则是重写了draw()方法,作用其实是一样的。

ViewGroup的工作原理

onMeasure()

  重写ViewGroup的onMeasure()方法时,必须先调用measureChildren()方法测量子组件的尺寸。该方法如下:

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);
        }
    }
}

  measureChildren()方法中,循环遍历每一个子组件,如果当前子组件的可见性不为GONE,也就是没有隐藏则继续调用measureChild()方法测量当前子组件的大小。measureChild()方法如下:

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);
}

  measureChild()方法结合父容器的MeasureSpec、子组件的padding和LayoutParams三个因素利用getChildMeasureSpec()方法计算出子组件的尺寸模式和尺寸大小,并调用子组件的measure()方法进行尺寸测量。measure()方法的实现如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //---;
    onMeasure(widthMeasureSpec,heightMeasureSpec);
    //---;
}

  measure()方法调用了onMeasure()方法,该方法正是我们重写的用来测量组件尺寸的方法。

  我们可以发现,从根元素出发,一步步向下递归驱动测量,每个组件又负责计算自身大小。

onLayout()

  接下来调用onLayout()方法定位子组件,以确定子组件的位置和大小,在onLayout()方法中,将会调用子组件的layout(),这里要一分为二,如果子组件是一个View,定位流程就到此结束,如果子组件又是一个容器,又会继续调用该容器的onLayout()方法对组件进行定位。所以,onLayout()方法也是一个递归的过程。

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    
    onLayout(changed, l, t, r, b);
    
}

onDraw()

  ViewGroup类并没有重写该方法,但是每一个组件在绘制的时候会调用View的draw()方法,draw()方法如下:

public void draw(Canvas canvas) {
    //---;
    dispatchDraw(canvas);
    //---;
}

  draw()方法中执行了语句dispatchDraw()方法,但是在View类中的dispatchDraw()方法是空方法,在ViewGroup类中,该方法是实现了的。如下:

protected void dispatchDraw(Canvas canvas) {
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    
    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
       
}

  dispatchDraw()方法的作用是将绘制请求分发到各子组件,并调用drawChild()方法来完成子组件的绘制,drawChild()方法的源码如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

  drawChild()方法再次调用了子组件的draw()方法,该方法如下:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    //---;
    if (drawingWithRenderNode) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
    } else {
        // Fast path for layouts with no backgrounds
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchDraw(canvas);
        } else {
            draw(canvas);
        }
    }
    //---;       
}

  最后也调用了draw()方法,如果子组件不再是一个容器,将调用onDraw()语句完成组件的绘制,同样的,onDraw()方法,正是我们需要重写的方法。组件的绘制同样是一个不断递归的过程。

重写onLayout()方法

  onLayout()方法的原型为:onLayout(boolean changed,int l,int t,int r,int b),其中参数changed判断是否有新的大小和位置变化,l表示left,t表示top,r表示right,b表示bottom,后面的四个参数表示容器自己相对父容器的位置以及自身的大小。通常情况下,r - l的值等同于方法getMeasuredWidth()方法的返回值,b - t的值等同于getMeasuredHeight()方法的返回值。

  在onLayout()方法中,需要调用View的layout()方法用于定义子组件和子容器的位置,layout()方法的原理如下:

public void layout(int l,int t,int r,int b)

  参数l、t、r、b四个参数的作用与上面相同,通过这四个参数,基本可以确定子组件的位置与大小了。

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
--------------------last line for now--------------------