Android 自定义ViewGroup

版本:1.0
日期:2014.4.2
版权:© 2014 kince 转载注明出处

之前介绍了关于自定义View类的一些知识,总结一下就是继承于View类,重写onDraw()方法。如果组件有自己的属性的话,在attrs.xml文件中加入即可。这类View一般只是用于显示一个位图或者一段文本,像TextView。实际开发中,用的不是很多,真正需求多的是像SliddingMenu侧滑菜单、下拉刷新等等。那这些组件一般情况下通常继承于ViewGroup或者ViewGroup的子类,子类分为两种情况,一种是像ListView、GridView等这样的数据显示组件;一中是Layout容器组件,如LinearLayout、RelativeLayout等。具体如何选择继承于那个类,和业务逻辑有密切关系。如果你想做一个ListView下拉刷新组件,那简单来说直接继承于ListView即可,如果想做一个侧滑菜单,那ListView显然不合适,所以就需要使用 ViewGroup或者Layout控件了。
既然这些组件都继承于 ViewGroup,因此对 ViewGroup的掌握尤为重要。首先看一下API文档,ViewGroup是View的子类,是布局和View容器的基础,它与View最重要的区别就是它可以做当一个容器包含其他子View。如图:

Android 自定义ViewGroup_第1张图片
而且它也定义了ViewGroup.LayoutParams类作为基类布局参数,如MATCH_PARENT、WRAP_CONTENT等。
FrameLayout应该都比较了解了,这是最简单的一个布局控件了,下面就自定义一个ViewGroup来实现一个简单的FrameLayout(API实例),代码如下:
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews;

/**
 * Example of writing a custom layout manager.  This is a fairly full-featured
 * layout manager that is relatively general, handling all layout cases.  You
 * can simplify it for more specific cases.
 */
@RemoteViews.RemoteView
public class CustomLayout extends ViewGroup {
    /** The amount of space used by children in the left gutter. */
    private int mLeftWidth;

    /** The amount of space used by children in the right gutter. */
    private int mRightWidth;

    /** These are used for computing child frames based on their gravity. */
    private final Rect mTmpContainerRect = new Rect();
    private final Rect mTmpChildRect = new Rect();

    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * Any layout manager that doesn't scroll will want this.
     */
    @Override
    public boolean shouldDelayChildPressedState() {
        return false;
    }

    /**
     * Ask all children to measure themselves and compute the measurement of this
     * layout based on the children.
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        // These keep track of the space we are using on the left and right for
        // views positioned there; we need member variables so we can also use
        // these for layout later.
        mLeftWidth = 0;
        mRightWidth = 0;

        // Measurement will ultimately be computing these values.
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        // Iterate through all children, measuring them and computing our dimensions
        // from their size.
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // Measure the child.
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                // Update our size information based on the layout params.  Children
                // that asked to be positioned on the left or right go in those gutters.
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.position == LayoutParams.POSITION_LEFT) {
                    mLeftWidth += Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                } else if (lp.position == LayoutParams.POSITION_RIGHT) {
                    mRightWidth += Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                } else {
                    maxWidth = Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                }
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
            }
        }

        // Total width is the maximum width of all inner children plus the gutters.
        maxWidth += mLeftWidth + mRightWidth;

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Report our final dimensions.
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }

    /**
     * Position all children within this layout.
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();

        // These are the far left and right edges in which we are performing layout.
        int leftPos = getPaddingLeft();
        int rightPos = right - left - getPaddingRight();

        // This is the middle region inside of the gutter.
        final int middleLeft = leftPos + mLeftWidth;
        final int middleRight = rightPos - mRightWidth;

        // These are the top and bottom edges in which we are performing layout.
        final int parentTop = getPaddingTop();
        final int parentBottom = bottom - top - getPaddingBottom();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                // Compute the frame in which we are placing this child.
                if (lp.position == LayoutParams.POSITION_LEFT) {
                    mTmpContainerRect.left = leftPos + lp.leftMargin;
                    mTmpContainerRect.right = leftPos + width + lp.rightMargin;
                    leftPos = mTmpContainerRect.right;
                } else if (lp.position == LayoutParams.POSITION_RIGHT) {
                    mTmpContainerRect.right = rightPos - lp.rightMargin;
                    mTmpContainerRect.left = rightPos - width - lp.leftMargin;
                    rightPos = mTmpContainerRect.left;
                } else {
                    mTmpContainerRect.left = middleLeft + lp.leftMargin;
                    mTmpContainerRect.right = middleRight - lp.rightMargin;
                }
                mTmpContainerRect.top = parentTop + lp.topMargin;
                mTmpContainerRect.bottom = parentBottom - lp.bottomMargin;

                // Use the child's gravity and size to determine its final
                // frame within its container.
                Gravity.apply(lp.gravity, width, height, mTmpContainerRect, mTmpChildRect);

                // Place the child.
                child.layout(mTmpChildRect.left, mTmpChildRect.top,
                        mTmpChildRect.right, mTmpChildRect.bottom);
            }
        }
    }

    // ----------------------------------------------------------------------
    // The rest of the implementation is for custom per-child layout parameters.
    // If you do not need these (for example you are writing a layout manager
    // that does fixed positioning of its children), you can drop all of this.

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CustomLayout.LayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    /**
     * Custom per-child layout information.
     */
    public static class LayoutParams extends MarginLayoutParams {
        /**
         * The gravity to apply with the View to which these layout parameters
         * are associated.
         */
        public int gravity = Gravity.TOP | Gravity.START;

        public static int POSITION_MIDDLE = 0;
        public static int POSITION_LEFT = 1;
        public static int POSITION_RIGHT = 2;

        public int position = POSITION_MIDDLE;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            // Pull the layout param values from the layout XML during
            // inflation.  This is not needed if you don't care about
            // changing the layout behavior in XML.
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
            gravity = a.getInt(R.styleable.CustomLayoutLP_android_layout_gravity, gravity);
            position = a.getInt(R.styleable.CustomLayoutLP_layout_position, position);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}
先不说它的实现逻辑,看一下里面都用到了哪些方法(参数先忽略):onMeasure()、getChildCount()、getChildAt()、measureChildWithMargins()、setMeasuredDimension()、onLayout()、getPaddingLeft()、getMeasuredWidth()、layout()等。从这些方法可以看出,有些是View类里面的,有些是ViewGroup类里面的。因此不是说自定义ViewGroup时候使用的方法都是ViewGroup里面的,因为ViewGroup里面也包含了View,子View类在设置自己的位置、大小等时候需要调用自己的方法。所以下面的重点将从View以及ViewGroup常用方法两个方面来展开。

一、View类

View类代表了用户界面组件的基本构建块,它占据了屏幕上的一个矩形区域,负责绘制和事件处理,是视图的基类部件,用来创建交互式UI组件(按钮、文本框等)。它的子类ViewGroup是布局的基类,是一个不可见的容器用来添加其他子View或者ViewGroup,并且定义他们的布局属性。
在屏幕Window中,所有的View都是存在于同一个视图树之中。可以使用代码或者xml布局去改变视图树的结构,动态的改变显示方式或者内容等。那一旦创建了一个视图树,通常可能会执行一些典型的常用操作:
1、设置属性: 设置控制的一些属性。
2、设置焦点:Android系统框架将处理移动焦点以响应用户输入,如果想强制获取焦点可以使用requestFocus()方法。

3、设置监听器:视图允许设置监听器来监听,当视图发生一些有趣的事情的时候。

4、设置可见性:可以使用setVisibility(int)隐藏或显示视图。

如果想要实现自定义View,可以通过重写View类的一些标准方法,这些方法有着各自的用途,在使用的时候根据功能来选择,如下图:
Android 自定义ViewGroup_第2张图片
onDraw(Canvas canvas):View类中用于重绘的方法,这个方法是所有View、ViewGroup及其派生类都具有的方法,也是Android UI绘制最重要的方法。开发者可重载该方法,并在重载的方法内部基于参数canvas绘制自己的各种图形、图像效果。

View的几何图形是一个矩形,有它自己固定的位置,表示为一对左、上坐标;和两个维度,表示为一个宽度和一个高度。位置和尺寸的单位都是像素。可以通过调用方法getLeft()和getTop()来检索一个视图的位置。前者返回到左边的距离或者这个View矩形的X坐标;后者返回到上面的距离或者视图矩形的Y坐标。需要注意的是,这些方法返回的位置都是相对于它的父视图而言的,而不是整个屏幕。例如,当getLeft()返回20,这意味着该视图到离它最近的父容器左边缘的距离是20。除此之外,为了避免一些不必要的计算,系统还提供了getRight() 和 getBottom()方法,他们返回到视图矩形右边缘和下边缘的坐标。例如,调用getRight()类似于以下计算:getLeft()+getWidth()。

关于View,有三个比较重要的属性:Size(大小), padding(填充) and margins(间隔)。视图的大小可以用高度和宽度去表示,一个视图实际上拥有两对宽度和高度值:第一对值是measured width 和 measured height。这些维度定义一个视图希望在其父视图里面有多大。可以通过使用getMeasuredWidth() 和 getMeasuredHeight()方法来获取这些维度。第二对值是width 和 height(有时是画的宽度和高度)。这些维度定义视图在屏幕上的实际大小,在绘图时和布局之后。这些值可以,但不需要和测量的宽度和高度(measured width and height)是不同的。宽度和高度可以通过调用 getWidth() and getHeight()方法获取。在测量视图维度的时候,padding(填充)是必须要考虑的,它以像素作为单位,到视图上下左右部分。可以使用setPadding(int, int, int, int) or setPaddingRelative(int, int, int, int)方法设置padding,另外可以通过使用 getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom(), getPaddingStart(), getPaddingEnd()方法来获取padding。不过需要注意的是视图虽然可以定义padding(填充),但是它没有定义margin的方法,定义margin是父容器的职责。可以参考 ViewGroup和 ViewGroup.MarginLayoutParams来了解。

当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点。绘制过程从布局的根节点开始,从根节点开始测量和绘制整个layout tree。每一个ViewGroup 负责要求它的每一个孩子被绘制,每一个View负责绘制自己。因为整个树是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。视图的布局分为两个过程:measure(测量)和layout(布局)。测量过程中,使用measure(int, int)方法,是一个自上而下的视图树的遍历。每个视图将尺寸规格在递归树。当这个测量过程结束时,每隔视图都存储了自己的大小。第二过程发生在layout(int,int,int,int),也是自顶向下。在这个过程中,每一个父容器通过使用在上一个过程中计算出来的数值来定位它的子视图。当一个视图的measure()方法返回时,其getMeasuredWidth()和getMeasuredHeight()值必须设置,以及这些视图的子节点。一个视图的measured width(宽度测量值)和measured height(高度测量值)必须尊重其父母容器的限制。这样可以保证在测量过程结束的时候,所有的父母都接受他们的孩子所有的测量。父容器可能不止一次使用measure()方法在子视图上面。比如,第一遍的时候,一个parent可能测量它的每一个孩子,并没有指定尺寸,parent只是为了发现它们想要多大;如果第一遍之后得知,所有孩子的无限制的尺寸总和太大或者太小,parent会再次对它的孩子调用measure()方法,这时候parent会设定规则,介入这个过程,使用实际的值。测量过程中使用两个类来和维度通信。 View.MeasureSpec类被子视图使用,用来通知他们的父类容器它们想要测量的大小以及位置。LayoutParams类用于描述一个视图的大小,LayoutParams是View用来告诉它的父容器它想要怎样被放置的参数。最基本的LayoutParams基类仅仅描述了View想要多大,即指明了尺寸属性。即View在XML布局时通常需要指明的宽度和高度属性。每一个维度都可以指定成下列三种值之一:
  1. 一个确切的数量
  2. MATCH_PARENT,这意味着要与其父一样大(减去填充)
  3. WRAP_CONTENT,这意味着认为希望是大到足以将其内容(加上填充)。
ViewGroup的不同子类(不同的布局类)有相应的LayoutParams子类,其中会包含更多的布局相关属性。
MeasureSpecs用于把需求在视图树上从父类到子视图遍历。MeasureSpecs有三种模式:
  1. UNSPECIFIED:这是父母用来确定所需的子视图的尺寸。例如,LinearLayout可能调用测量()在其孩子的高度不确定,宽度240找出给定的子视图要多高的宽度为240像素。
  2. EXACTLY:这是使用的父母对孩子施加一个精确的尺寸。孩子必须使用这个尺寸,保证所有的后代会适合这个尺寸。
  3. AT_MOST:这是使用的父母对孩子施加一个最大尺寸。孩子必须保证它和它的所有后代会适合这个尺寸。
可以通过使用requestLayout()方法来初始化一个layout。

二、ViewGroup类

本文开篇时,已经介绍了一个自定义ViewGroup的例子,但并没有具体介绍,因为里面还涉及到一些关于View类的东西在里面.上面已经把View体系大体梳理了一遍,下面开始正式将二者结合起来。但是不要忘记ViewGroup是继承与View的,所以要明白二者的共性与区别。ViewGroup是一个抽象类,它有两个重要的方法:onLayout和onMeasure,前者是必须重写实现的。ViewGroup有几个重要的方法如下:

1、onMeasure(int widthMeasureSpec, int heightMeasureSpec)

onMeasure方法是测量view和它的内容,再详细说就是获得ViewGroup和子View的宽和高(measured width和measured height) ,然后设置ViewGroup和子View的宽和高。这个方法由 measure(int, int)方法唤起,子类可以覆写onMeasure来提供更加准确和有效的测量。onMeasure传递进来的参数widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值,是对得出来的测量值的限制。一般是根据xml文件中定义得到的,可以根据这2个参数知道模式和size。需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size = MeasureSpec.getSize(widthMeasureSpec)得到尺寸。分别是parent提出的水平和垂直的空间要求。这两个要求是按照View.MeasureSpec类来进行编码的。参见View.MeasureSpec这个类的说明:这个类包装了从parent传递下来的布局要求,传递给这个child。每一个MeasureSpec代表了对宽度或者高度的一个要求。每一个MeasureSpec有一个尺寸(size)和一个模式(mode)构成。MeasureSpecs这个类提供了把一个的元组包装进一个int型的方法,从而减少对象分配。当然也提供了逆向的解析方法,从int值中解出size和mode。mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。对应关系:
  1. xml文件中的wrap_content-----MeasureSpec.AT_MOST:Child可以是自己任意的大小,但是有个绝对尺寸的上限。
  2. xml文件中的match_parent-----MeasureSpec.EXACTLY:Parent为child决定了一个绝对尺寸,child将会被赋予这些边界限制,不管child自己想要多大。
  3. xml文件中的-----MeasureSpec.UNSPECIFIED:这说明parent没有对child强加任何限制,child可以是它想要的任何尺寸。
在覆写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。如果没有这么做,将会由measure(int, int)方法抛出一个IllegalStateException。覆写onMeasure方法的时候,子类有责任确保measured height and width至少为这个View的最小height和width。(getSuggestedMinimumHeight() and getSuggestedMinimumWidth())。典型的onMeasure的一个实现:
@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
 
    int width = MeasureSpec.getSize(widthMeasureSpec);   //获取ViewGroup宽度 
    int height = MeasureSpec.getSize(heightMeasureSpec);  //获取ViewGroup高度 
    setMeasuredDimension(width, height);    //设置ViewGroup的宽高 
 
    int childCount = getChildCount();   //获得子View的个数,下面遍历这些子View设置宽高 
    for (int i = 0; i < childCount; i++) { 
        View child = getChildAt(i); 
            child.measure(viewWidth, viewHeight);  //设置子View宽高 
        } 
}
很明显,先获取到了宽高再设置。顺序是先设置ViewGroup的,再设置子View。其中,设置ViewGroup宽高的方法是 setMeasureDimension(),查看这个方法的源代码,它在View.java下:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 
        boolean optical = isLayoutModeOptical(this); 
        if (optical != isLayoutModeOptical(mParent)) { 
            Insets insets = getOpticalInsets(); 
            int opticalWidth  = insets.left + insets.right; 
            int opticalHeight = insets.top  + insets.bottom; 
 
            measuredWidth  += optical ? opticalWidth  : -opticalWidth; 
            measuredHeight += optical ? opticalHeight : -opticalHeight; 
        } 
        mMeasuredWidth = measuredWidth;  //这就是保存到类变量
        mMeasuredHeight = measuredHeight; 
 
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; 
}
setMeasureDimension方法必须由onMeasure调用,上面的代码刚好是在onMeasure中调用,所以才符合要求。那设置的这个宽高保存在哪里呢?源代码中也可以看出,它保存在ViewGroup中:mMeasuredWidth,mMeasuredHeight是View这个类中的变量。接下来是设置子View的宽高,每个子View都会分别设置,这个宽高当然是自己定义的。child.measure(viewWidth, viewHeight);调用的是measure方法,注意这个方法是属于子View的方法,那设置的高度保存在哪里呢?对了,就是每个子View中,而不是ViewGroup中,这点要分清楚。再来看看measure的实现:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// measure ourselves, this should set the measured dimension flag back 
    
onMeasure(widthMeasureSpec, heightMeasureSpec); 
}
可以看到,其实它又调用了View类中的onMeasure方法,在看View.java的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
}
和上面说的一样,使用getDefaultSize()方法确保有高度和宽度。就这样,不断地调用measure()->onMeasure()->setMeasuredDimension()来测量保存宽高度值的,这就是之前说的递归遍历。
在Android提供的一个自定义View示例中(在API demos 中的 view/LabelView),可以看到一个重写onMeasure()方法,实例,也比较好理解:
 /**
     * @see android.view.View#measure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * Determines the width of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    /**
     * Determines the height of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
2、 onLayout(boolean changed, int left, int top, int right, int bottom)

View类中布局发生改变时会调用的方法,用于设置子View的位置,这个方法是所有View、ViewGroup及其派生类都具有的方法,重载该类可以在布局发生改变时作定制处理,这在实现一些特效时非常有用。它是设置子View的大小和位置。onMeasure只是获得宽高并且存储在它各自的View中,这时ViewGroup根本就不知道子View的大小,onLayout告诉ViewGroup,子View在它里面中的大小和应该放在哪里。子view,包括孩子在内,必须重写onLayout(boolean, int, int, int, int)方法,并且调用各自的layout(int, int, int, int)方法。参数说明:参数changed表示view有新的尺寸或位置;参数l表示相对于父view的Left位置;参数t表示相对于父view的Top位置;参数r表示相对于父view的Right位置;参数b表示相对于父view的Bottom位置。这些位置默认是0,除非你在ViewGroup中设置了margin。一个典型实现如下:
@Override   
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {   
    int mTotalHeight = 0;   
    // 当然,也是遍历子View,每个都要告诉ViewGroup   
    int childCount = getChildCount();   
    for (int i = 0; i < childCount; i++) {   
        View childView = getChildAt(i);   
        // 获取在onMeasure中计算的视图尺寸   
        int measureHeight = childView.getMeasuredHeight();   
        int measuredWidth = childView.getMeasuredWidth();   
        childView.layout(left, mTotalHeight, measuredWidth, mTotalHeight + measureHeight);       
        mTotalHeight += measureHeight;   
    }   
}

3、dispatchDraw(Canvas canvas)

ViewGroup类及其派生类具有的方法,这个方法主要用于控制子View的绘制分发,重载该方法可改变子View的绘制,进而实现一些复杂的视效,典型的例子可参见Launcher模块Workspace的dispatchDraw重载。默认情况下,ViewGroup已经实现了这个方法,所以不是有特别的需求,是不需要重写这个方法的,这个方法部分代码如下:
 /**
     * {@inheritDoc}
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;

            final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, count);
                    bindLayoutAnimation(child);
                    if (cache) {
                        child.setDrawingCacheEnabled(true);
                        if (buildCache) {                        
                            child.buildDrawingCache(true);
                        }
                    }
                }
            }

可以看到,默认实现已经做了很多工作了,主要是视图的绘制。

4、drawChild(Canvas canvas, View child, long drawingTime))

ViewGroup类及其派生类具有的方法,这个方法直接控制绘制某局具体的子view,重载该方法可控制具体某个具体子View。

5、getChildCount()

获取子View的个数

6、getChildAt()

方法 这个方法用来返回指定位置的View。注意:ViewGroup中的View是从0开始计数的。

7、onSizeChanged(int, int, int, int)

当View大小改变时,调用此方法

8、measure(int widthMeasureSpec, int heightMeasureSpec)

测量视图的大小,并保存起来

9、layout(int l, int t, int r, int b)

布置视图的位置

10、getMeasuredHeight()
获取测量的高度

11、getMeasuredWidth()

获取测量的宽度

12、measureChildren(int widthMeasureSpec, int heightMeasureSpec)

看一下这个方法的代码就一目了然了:
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);
            }
        }
    }
没错,就是循环调用measureChild方法而已。

13、measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)

measure(int widthMeasureSpec, int heightMeasureSpec)的改进方法,考虑到padding和margin。

14、measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)

方法代码如下:
  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);
    }
也是一个循环调用的过程,调用的是child.measure方法。所以一般情况下,通过直接使用measureChildren方法来调用measureChild方法,就可以实现测量子View的大小。
15、scrollBy(int x, int y)


16、scrollTo(int x, int y)

总结:

1、View中包含onLayout()、onMeasure()、layout()、measure()。查看源码可以在View的layout()中调用了onLayout(),而onLayout()本身是一个等待重写的空方法,同样的在measure()中调用了onMeasure()方法,和onLayout()不同的是onMeasure()并不是一个空方法,在其中调用了setMeasureDimension()方法。setMeasureDimension()是用来保存组件的widthMeasureSpec和heightMeasueSpac信息的,这两个保存下来的信息就是getMeasuredWidth()和getMeasuredHeight()中返回的值。

2、ViewGroup和View是一样的,不同的是ViewGroup中onLayout是一个必须被实现的抽象方法。同样的在layout()中调用onLayout(),在measure()中调用onMeasure()。

3、要实现自定义的ViewGroup,可以在其必须实现的onLayout()中调用子View的layout()。而子view的layout()又会去调用其onLayout()方法,那么这样就形成了所谓的“递归”布局过程了。

4、一个view的真正布局方法是setFrame(),这在layout()中有被调用到。即layout()中会先调用setFrame()设置组件的边界,再调用onLayout(),如果该view包含子view。则可以调用每个子view的layout()方法。

5、要指子view的大小可以在子view的onMeasure()中调用setMesauredDimension()来实现。

6、综上所述,android中组件绘制分为两个过程,测量(Measure)和布局(Layout)。测量过程通过measure()及onMeasure()方法实现,measure()方法中调用了onMeasure()方法,在onMeasure()中可以通过setMeasuredDimension()来设置组件的尺寸信息,这些尺寸信息在layout过程中会被使用到。布局过程是通过layout()及onLayout()方法实现的,layout()中调用了onLayout(),在layout()中可以使用getMeasuredHeight()或getMeasuredWidth()通过setFrame()来设置组件范围。所以在自定义的ViewGroup中往往只重写onLayout(),这是因为ViewGroup的layout()方法已经被系统调用了。

7、ViewGroup绘制 的过程是这样的:onMeasure → onLayout → DispatchDraw。onMeasure负责测量这个ViewGroup和子View的大小,onLayout负责设置子View的布局,DispatchDraw就是真正画上去了。


三、实例

下面根据上面基础,开始正式自定义ViewGroup,首先自定义一个类继承于ViewGroup,并重写onLayout()方法:
package com.example.viewgroupdemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;

public class CustomViewGroup extends ViewGroup {

public CustomViewGroup(Context context) {
super(context);
// TODO Auto-generated constructor stub
}

public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}

public CustomViewGroup(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}

@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
// TODO Auto-generated method stub

}

}

然后在MainActivity中引用,代码如下:
package com.example.viewgroupdemo;

import android.os.Bundle;
import android.app.Activity;
import android.graphics.Color;
import android.widget.ImageView;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mCustomViewGroup);
}

}

xml:
android:layout_width="match_parent"
android:layout_height="match_parent" >



android:layout_width="50dp"
android:layout_height="100dp"
android:background="@drawable/ic_launcher"
android:text="l1" />



android:layout_width="50dp"
android:layout_height="100dp"
android:background="@drawable/ic_launcher"
android:text="l2" />


运行之后发现,程序异常退出,log输出如下:

也就是之前所说的,必须要设置setMeasureDimension()方法。那对代码做修改,在CustomViewGroup中设置setMeasureDimension()方法,代码如下:
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

          int width = MeasureSpec.getSize(widthMeasureSpec);
          Log.i("onMeasure--width", width + "");
          int height = MeasureSpec.getSize(heightMeasureSpec);
          Log.i("onMeasure--height", height + "");
          setMeasuredDimension(width, height);
          measureChildren(widthMeasureSpec, heightMeasureSpec);  
     }
然后运行程序,发现没有任何显示,
也就是说,子元素根本就没有被绘制上去。注意到上面有一个要求重载的方法onLayout(),之前在这个方法里面,什么都没有做。根据onLayout()方法的介绍,子view,包括孩子在内,必须重写onLayout(boolean, int, int, int, int)方法,并且调用各自的layout(int, int, int, int)方法。那么现在就在onlayout()方法里面去调用,代码如下:
     @Override
     protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
          // TODO Auto-generated method stub

          for (int i = 0; i < getChildCount(); i++) {
               View v=getChildAt(i);
               v.layout(arg1, arg2, arg3, arg4);
          }
     }
运行程序,图片可以显示了:
Android 自定义ViewGroup_第3张图片
不过,两个TextView重合了,也就是他们的位置是一样的,而且还是全屏显示的。那先分析一下原因。在v.layout(arg1, arg2, arg3, arg4);函数中,arg3、arg4返回的分别是屏幕的宽和高。所以TextView显示出来的是全屏。下面将onLayout方法改一下,
       @Override
     protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
          // 屏幕方向改变时候会调用
          Log.i("onLayout--arg0", arg0 + "");
          Log.i("onLayout--arg1", arg1 + "");
          Log.i("onLayout--arg2", arg2 + "");
          Log.i("onLayout--arg3", arg3 + "");
          Log.i("onLayout--arg4", arg4 + "");

          int childCount = getChildCount();
          Log.i("onLayout--childCount", childCount + "");
         
          for (int i = 0; i < childCount; i++) {
               View view = getChildAt(i);
               int chiledWidth = view.getMeasuredWidth();
               Log.i("onLayout--chiledWidth", chiledWidth + "");
               int chiledHeight = view.getMeasuredHeight();
               Log.i("onLayout--chiledHeight", chiledHeight + "");
               view.layout(arg1, arg2, chiledWidth, chiledHeight);
          }
     }
使之显示自己设置的大小,运行结果如下:
Android 自定义ViewGroup_第4张图片


你可能感兴趣的:(Android 自定义ViewGroup)