android 自定义View开发实战(三) 自定义ViewGroup--FourLayout

下面我们定义一个可以容纳4个view的Layout,分别位于四个角。这个实例主要是为了让大家理解如何自定义一个ViewGroup。

首先需要了解下ViewGroup的职责。

ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是容器的类型),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式;决定childView的位置;

为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。

ViewGroup和LayoutParams之间的关系

大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams,RelativeLayout有RelativeLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。

1.   定义FourLayout类继承ViewGroup

public class FourLayout extends ViewGroup {
    private Context mContext;

    public FourLayout(Context context){
        this(context,null);
    }

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

    public FourLayout(Context context,AttributeSet attrs,int defStyle){
        super(context,attrs,defStyle);
        mContext = context;
    }
……
}
这里只是简单的复写了三个构造方法,一般来说,这三个构造方法是我们自定义view经常复写的。

2.   指定相应的LayoutParams

因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。因此,需要重写ViewGroup的LayoutParamsgenerateLayoutParams()方法,用于返回LayoutParams。

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //返回MarginLayoutParams
        return new MarginLayoutParams(getContext(),attrs);
    }
这里我们返回MarginLayoutParams的实例,因为我们的ViewGroup只需要支持margin即可。这样就为我们的ViewGroup指定了其LayoutParams为MarginLayoutParams。

3.   重写onMeasure()

根据ViewGroup的职责,我们需要在onMeasure()中测量子View,并且根据子View的布局,我们要计算出我们自己的宽和高。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //测量各个子元素的宽高
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        /**
         * 下面是根据子元素测量后,测量自己的宽高
         */
        int width = 0;
        int height = 0;
        MarginLayoutParams param = null;

        //左 右的高度
        int leftHeight = 0;
        int rightHeight = 0;

        //上 下 的宽度
        int topWidth = 0;
        int bottomWidth = 0;

        for (int i = 0;i < getChildCount();i++){
            //获取子View
            View child = getChildAt(i);
            param = (MarginLayoutParams) child.getLayoutParams();

            //最上面两个
            if (i == 0 || i == 1){
                topWidth += param.leftMargin + child.getMeasuredWidth() + param.rightMargin;
            }

            //最左边两个
            if (i == 0 || i == 2){
                leftHeight += param.topMargin + child.getMeasuredHeight() + param.bottomMargin;
            }

            //最下面两个
            if (i == 2 || i == 3){
                bottomWidth += param.leftMargin + child.getMeasuredWidth() + param.rightMargin;
            }

            //最上面两个
            if (i == 1 || i == 3){
                rightHeight += param.topMargin + child.getMeasuredHeight() + param.bottomMargin;
            }
        }

        //获取两者的最大值
        width = Math.max(topWidth,bottomWidth);
        height = Math.max(leftHeight,rightHeight);

        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        }

        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        }

        setMeasuredDimension(width,height);
    }

首先获取该ViewGroup父容器为其设置的计算模式和尺寸,大多情况下,只要不是wrap_content,父容器都能正确的计算其尺寸。所以我们自己需要计算如果设置为wrap_content时的宽和高,如何计算呢?那就是通过其childView的宽和高来进行计算。

通过ViewGroup的measureChildren方法为其所有的childView设置宽和高,此行执行完成后,childView的宽和高都已经正确的计算过了

根据childView的宽和高,以及margin,计算ViewGroup在wrap_content时的宽和高。

如果宽高属性值为wrap_content,则设置为上面计算的值,否则为其父容器传入的宽和高。

 

4.   重写onLayout()

对于继承ViewGroup的,要求必须实现onLayout()方法,因此这里是必须的。在这里我们必须指明各个子View是如何放置的,其实就是指明各个childView的left,top,right,bottom。然后最后调用各个childView的layout()方法来进行Layout,一旦layout后,各个childview就固定好了。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childWidth = 0;
        int childHeight = 0;
        MarginLayoutParams param = null;

        /**
         * 遍历所有子View,根据其宽高对其进行margin布局
         */
        for (int i = 0;i < getChildCount();i++){
            View child = getChildAt(i);
            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            param = (MarginLayoutParams) child.getLayoutParams();

            int left = 0;
            int right = 0;
            int top = 0;
            int bootom = 0;

            switch (i){
                case 0:
                    left = param.leftMargin;
                    top = param.topMargin;
                    break;
                case 1:
                    left = getMeasuredWidth() - childWidth - param.rightMargin;
                    top = param.topMargin;
                    break;
                case 2:
                    left = param.leftMargin;
                    top = getMeasuredHeight() - childHeight - param.bottomMargin;
                    break;
                case 3:
                    left = getMeasuredWidth() - childWidth - param.rightMargin;
                    top = getMeasuredHeight() - childHeight - param.bottomMargin;
                    break;
                default:
                    break;
            }

            right = left + childWidth;
            bootom = top + childHeight;

            //调用child的layout完成子view的layout
            child.layout(left,top,right,bootom);
        }

    }

这里的工作就是遍历所有的childView,根据childView的宽和高以及margin,然后分别将0,1,2,3位置的childView依次设置到左上、右上、左下、右下的位置。

如果是第一个View(index=0) :left = param.leftMargin,top = param.topMargin,right = childView.getWidth() + left,bootom =childView.getHeight() + top;

如果是第二个View(index=1) : left = getMeasuredWidth()- childWidth - param.rightMargin, top = param.topMargin,right =childView.getWidth() + left,bootom = childView.getHeight() + top;

其他依次类似。

计算出了left,top,right,bootom,之后,我们需要调用childView的layout()方法来进行layout过程。


这样就完成了,我们的ViewGroup代码的编写,下面我们进行测试,分别设置宽高为固定值,wrap_content,match_parent。

5.   使用自定义的FourLayout

直接在布局文件中引入自定义的ViewGroup

    

        

        

        

        

    

效果如下图:
android 自定义View开发实战(三) 自定义ViewGroup--FourLayout_第1张图片

最后源代码见github: 点击打开链接

你可能感兴趣的:(android,UI开发)