自定义ViewGroup

1、什么是ViewGroup?

官方定义:

/**
A ViewGroup is a special view that can contain other views (called children.) 
The view group is the base class for layouts and views containers. 
This class also defines the ViewGroup.
LayoutParams class which serves as the base class for layouts parameters. 
*/

一句话就是,ViewGroup是一个存放view的容器,同时它又是view的子类,所以是一个特殊的view,layout布局文件都是从它继承而来的。(比如说LinearLayout等)

2、如何自定义ViewGroup

主要是重写onMeasure()和onLayout()两个函数,那这两个函数分别是做什么的呢?

文档上是这样描述onMeasure的,

protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
/**
*Measure the view and its content to determine the measured width and the measured height. 
*This method is invoked by measure(int, int) and should be overridden by subclasses
*to provide accurate and efficient measurement of their contents. 
*/

直译的意思就是它的内容决定了它本省测量的宽和高,这个方法由measure(int,int)唤醒,在子类中应该重写它,通过准确有效的测量它的内容(或者说是子View)来确定自己的mesaure。

简单说就是,这个方法是来确定自身的宽和高的,就是通过测量所有子View的宽和高,来计算得到自己的宽和高。

那具体是怎样测量的呢?

首先要知道测量的步骤,分两步,首先确定测量模式,然后根据模式在进行测量计算。说到这,先说一说测量模式是怎么回事吧,还记得我们在设置TextView,Button,LinearLayout等等这些View的时候都要指定layout_width和layout_height吗?可以等于wrap_content,fill_parent,40dp等等。这里面就包含了测量模式的信息。

view的测量模式一共分三种EXACTLY、AT_MOST、UNSPECIFIED。先看看文档中是怎样说的吧。

public static final int AT_MOST
Added in API level 1
Measure specification mode: The child can be as large as it wants up to the specified size.
Constant Value: -2147483648 (0x80000000)

public static final int EXACTLY
Added in API level 1
Measure specification mode: The parent has determined an exact size for the child. 
The child is going to be given those bounds regardless of how big it wants to be.
Constant Value: 1073741824 (0x40000000)

public static final int UNSPECIFIED
Added in API level 1
Measure specification mode: The parent has not imposed any constraint on the child. 
It can be whatever size it wants.
Constant Value: 0 (0x00000000)
1、EXACTLY: 精确测量,例如我们指定 100dp, match_parent  ,不依据子View的大小,子View的大小受父ViewGroup的限制
2、AT_MOST:子View可以想多大就多大 wrap_content,根据子View的大小确定自己的大小
3、UNSPCIFIED:子控件想要多大就多大,比如ScrollView中的子控件,自身的大小也不确定
onMeasure的大致写法如下:
@Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		
		int measuredWidth = 0;   //存储计算后自己的宽
		int measuredHeight = 0;  //存储计算后自己的高
		//开始计算自己的宽和高
		.
		.
		.
		//完成计算自己的宽和高
		setMeasuredDimension(measuredWidth, measuredHeight);//根据计算出来的宽和高来设置自己的宽和高
	}
大家有没有疑问,一个整形变量怎么就可以既确定模式又确定大小的呢。这里就拿widthMeasureSpec做下解释,这个值由高32位和低16位组成,高32位保存的值叫specMode,可以通过如代码中所示的 MeasureSpec.getMode()获取;低16位为specSize,同样可以由MeasureSpec.getSize()获取。

那widthMeasureSpec, heightMeasureSpec这两个参数是从哪里来的?

onMeasure()函数由包含这个View的具体的ViewGroup调用,因此值也是 从这个ViewGroup中传入的。子类View的这两个参数,由ViewGroup中的 layout_width,layout_height和padding以及View自身的layout_margin共同决定。当然权值weight也是尤其需要考虑的因素。
确定完测量模式,测量出自身的宽和高后,当然要将自己的宽和高注册一下,这就需要调用setMeasureDimension(int meauredWidth,int measuredHeght)。


通过onMeasure确定出自身的大小后,接下来就需要确定每个子View在ViewGroup的位置啦,这就需要onLayout来实现了。

protected void onLayout (boolean changed, int left, int top, int right, int bottom)
Added in API level 1

Called from layout when this view should assign a size and position to each of its children. 
Derived classes with children should override this method and call layout on each of their children.
直译:为每一个子View分配位置和大小,继承类中要重写这个方法,在为每个子View调用layout方法来分配位置。

3、实例,自定义流式布局FlowLayout

了解了怎样自定义ViewGroup后那就让我动手实践一下吧,我们做一个流式布局。

就是说可以向这个布局中添加一串子View,而每一行的子View个数是不确定的,是由子View本身大小来确定的。先看看效果吧。

3.1 效果展示


3.2 编码实现

onMeasure(int widthMeasureSpec,int heightMeasureSpec)方法中得到宽和高的大致思路就是:

根据测量模式先分为两类(这里不考虑UNSPCIFIED模式),如果是具体值或者fill_parent(EXACTLY模式),则直接通过

 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

得到自身的宽和高,结束;

如果是wrap_content(AT_MOST模式),则需要根据子View的高和宽来计算自己的高和宽,具体思路是:叠加子View的width,直到叠加的宽>widthSize,就记录本行的宽,并获取本行中所有子View高的最大值为本行的高,然后换行,重复上面的过程,最后得到所有行中最宽的作为本ViewGroup的宽,将所有行的高加起来作为本ViewGroup的高,结束。

具体代码:

  @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //wrap_content
        int width = 0;
        int height = 0;
        //记录每一行的宽度和高度
        int lineWidth = 0;
        int lineHeight = 0;

        //得到内部子View的个数
        int cCount = getChildCount();
        for(int i = 0;i<cCount;i++){
            View child = getChildAt(i);
            //测量子View的宽和高
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
            //得到子View的LayoutParams
            MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();//子View.getLayoutParams(),得到是父ViewGroup的LayoutParams类型
            //子View占据的宽度和高度
            Log.e(TAG1,"getMeasuredWidth:"+child.getMeasuredWidth());
            Log.e(TAG1,"getMeasuredHeight:"+child.getMeasuredHeight());
            Log.e(TAG1,"getWidth:"+getWidth());
            Log.e(TAG1,"getHeight:"+getHeight());
            Log.e(TAG1, "child.getWidth():" + child.getWidth());
            Log.e(TAG1,"child.getHeight:"+child.getHeight());
            int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
            int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;
            //换行
            if(lineWidth+childWidth>widthSize-getPaddingLeft()-getPaddingRight()){
                //疑问,当宽度是wrap_cotent时,这个widthSize的值是多少呢?分别与fill_parent 、60dp比较试试
                //实验发现,wrap_content和fill_parent得到的值是一样的,60dp得到对应的像素值
                //对比得到最大值
                width = Math.max(width,lineWidth);
                //重置lineWidth,因为当前的子View要在下一行显示,并且作为开头
                lineWidth = childWidth;
                //记录整体高度
                height += lineHeight;
                lineHeight = childHeight;
            }else{//未换行
                //叠加行宽
                lineWidth+=childWidth;
                //得到当前最大的高度
                lineHeight = Math.max(lineHeight,childHeight);
            }
            //最后一个子View
            if(i==cCount-1){
                width = Math.max(width,lineWidth);
                height +=lineHeight;
            }
        }
        Log.e("TAG","widthSize = "+widthSize);
        Log.e("TAG","heightSize = "+heightSize);

        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY?widthSize:width+getPaddingRight()+getPaddingLeft(),
                heightMode==MeasureSpec.EXACTLY?heightSize:height+getPaddingBottom()+getPaddingTop());
    }

onLayout()分配每个子View的位置并绘制的思路就是两步,第一步计算每个子View的大小,按行分配好,即确定好每个子View的位置;第二步,就是根据确定好的位置,进行绘制。具体代码:
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mAllView.clear();
        mLineHeight.clear();

        //当前ViewGroup的宽度
        int width = getWidth();//???为什么用getWidth不用getMeasuredWidth()
                               //getWidth()得到的是ViewGroup的实际宽度,而getMeasuredWidth()得到的是它期望得到的宽度
                               //当模式为EXACTLY时,二者相同
        int lineWidth = 0;
        int lineHeight = 0;
        List<View> lineViews = new ArrayList<View>();//一行的子View
        int cCount = getChildCount();
        //确定每个子View的位置
        for(int i =0;i<cCount;i++){
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            if(lineWidth+childWidth+lp.leftMargin+lp.rightMargin>width-getPaddingRight()-getPaddingLeft()) {//如果需要换行
                mLineHeight.add(lineHeight);//记录当前行高
                mAllView.add(lineViews);//将当前行的Views 记录到父View中
                lineWidth = 0;//重置行宽和高
                lineHeight = childHeight + lp.topMargin+lp.bottomMargin;
                lineViews = new ArrayList<View>();
            }
            lineWidth += childWidth+lp.leftMargin+lp.rightMargin;
            lineHeight = Math.max(lineHeight,childHeight+lp.topMargin+lp.bottomMargin);
            lineViews.add(child);//记录当前行的View
            Log.e(TAG2,"getMeasuredWidth:"+child.getMeasuredWidth());
            Log.e(TAG2, "child.getMeasuredHeight:" + child.getMeasuredHeight());
            Log.e(TAG2,"getWidth:"+getWidth());
            Log.e(TAG2, "getHeight:" + getHeight());
            Log.e(TAG2, "child.getWidth():" + child.getWidth());
            Log.e(TAG2,"child.getHeight:"+child.getHeight());
        }
        //处理最后一行
        mLineHeight.add(lineHeight);
        mAllView.add(lineViews);
        //设置子View的位置
        int left = getPaddingLeft();
        int top = getPaddingTop();
        //行数
        int lineNum = mAllView.size();
        //逐个进行绘制
        for(int i=0;i<lineNum;i++){
            lineViews = mAllView.get(i);
            lineHeight = mLineHeight.get(i);
            for(int j=0;j<lineViews.size();j++){
                View child = lineViews.get(j);
                //判断child的状态
                if(child.getVisibility()==View.GONE){
                    continue;
                }
                MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
                int lc = left +lp.leftMargin;
                int tc = top +lp.topMargin;
                int rc = lc+child.getMeasuredWidth();
                int bc = tc+child.getMeasuredHeight();
                //为view布局
                child.layout(lc,tc,rc,bc);
                left +=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
                Log.e(TAG3, "getMeasuredWidth:" + child.getMeasuredWidth());
                Log.e(TAG3,"getMeasuredHeight:"+child.getMeasuredHeight());
                Log.e(TAG3, "getWidth:" + getWidth());
                Log.e(TAG3, "getHeight:" + getHeight());
                Log.e(TAG3, "child.getWidth():" + child.getWidth());
                Log.e(TAG3,"child.getHeight:"+child.getHeight());
            }
            left = getPaddingLeft();
            top += lineHeight;
        }
    }

3.3 参数解释

(1)getWidth()和getMeasuredWidth()的区别及用法。

getWidth()得到的是ViewGroup的实际宽度,而getMeasuredWidth()得到的是它期望得到的宽度,当模式为EXACTLY时,二者相同;但是ViewGroup的getWidth()只有在onMeasure中执行完setMeasuredDimension之后才会有值,对于子View来说只有在onLayout()中为子View执行完布局之后(layout()后)才有值。

(2)模式是wrap_cotent时,MeasureSpec.getSize(widthMeasureSpec)得到的值分别与fill_parent 、60dp有什么不同?
实验发现,wrap_content和fill_parent得到的值是一样的,60dp得到对应的像素值

致谢:感谢慕课的鸿神,这篇文章也是学习鸿神的课整理总结写出来的。

源代码下载


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值