自定义view用了好久,用着用着吧感觉也就那么回事儿啊,如果想画个view,那就得知道view的大小。

那么首当其中的就是onMeasure()方法啦。
先上一张图,这是我当年自学android的时候看的黑马视频的图。珍藏了快两年啦

这里写图片描述

这张图,第一次看基本看不懂

onMeasure有三种 模式

  • EXACTLY:精确 父容器已经知道 这个view的大小

  • AT_MOST:父容器 定了一个最大值,view不能大于这个值

  • UPSPECIFIED:父容器 不对view做限制,view想多大就多大 不用管这个模式,用不上

    简单说上面的图怎么看呢:(不用看UPSPECIFIED)

    先看子view的layoutParams(中间那列)

    如果xml里面写的 是 dip 那么该view都是EXACTLY

    如果xml里面写的 是 wrap_content 那么该view都是AT_MOST

    如果xml里面写的 是 match_parent 那么子view就和父容器的一样


View的onMeasure()方法

先说一句啊,其实xml存在的意义呢 就是 让我们开发方便一些,可视化一些,mvc一些。所以呢xml里面的数值 属性都会 在代码里面提现,所以 xml 和代码同时 设置了 某个属性 那么 大多数都会以 代码为准(说话永远都不能太绝对)。

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

//这个就是测量啦
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}
注意啦:widthMeasureSpec和heightMeasureSpec 并不是真正的宽高。是一个32int值。高2位 代表SpecMode,低30为代表SpecSize 。一会儿会用到

我们看看super 里面

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

其实呢 setMeasuredDimension(int,int)就是设置这个view的大小啦。 现在我们这样写

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(100,100); 
//这样写呢 ,不管你xml里面的宽高些什么,都不管用了。view的宽高永远是100,100; 不信可以试一下
    }
}

还有一个要注意的就是

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

这个getDefaultSize();是什么意思呢 进去看看


 public static int getDefaultSize(int size, int measureSpec) {
        int result = size;//给了一个默认值
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
//上面说的还记得吧,measureSpec是一个32为的int值 。MeasureSpec.getMode和MeasureSpec.getSize方法 分别获取 模式 和 真实的大小。
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED://如果是不限制大小 就给他一个默认值
            result = size;
            break;
        case MeasureSpec.AT_MOST: //如果xml里面写的 是 wrap_content 那么该view都是AT_MOST
        case MeasureSpec.EXACTLY://如果是精确的 那就是 精确的了
            result = specSize;
            break;
        }
        return result;
    }

所以你会发现。如果你的view宽高是 wrap_content那么他的大小就是 父空间剩余的大小一样大了(要减去 padding marging)

这里写图片描述

好啦。现在知道view 空间是如何 测量大小的了吧。 然后呢我们可以根据getDefaultSize()的 内容自己写一个。这里 改了一下 《android英雄传》里的代码 ,以后不要再因为onMeaure不会写 不敢自定义view啦。下面的代码直接复制 。上面解释 不看也行

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(200, widthMeasureSpec), measureHight(200, heightMeasureSpec));
    }

    private int measureWidth(int size, int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
            System.out.println("EXACTLY:" + result);
        } else {
            result = size;//最小值是200px ,自己设定
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);

            }
        }

        return result;
    }

    private int measureHight(int size, int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = size;//最小值是200px ,自己设定
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

如果你想弄个 jar包啊,复用的库啊 直接用上面方法,
如果你追求没那么高 直接用 setMeasuredDimension(int, int);就行啦。


ViewGroup的onMeasure

也很简单

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        this(context, null);
    }

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

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(100, 200);
    }

其实和view的完全一样。这个时候 viewgroup的宽高就是100200了

如果这样呢:
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

   这个时候呢 ,不管xml里面不管是wrap_content,还是match_parent  这个自定义viewgroup的大小都是 match_parent 

再进一步呢:测量子view的布局
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
//                挨个测量大小.好简单有没有
                measureChildren(widthMeasureSpec, heightMeasureSpec);
            }
        }
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

下面直接一步到位:
我们的自定义viewgroup考虑的问题
- padding margin
- 父窗体要在wrap_content的时候要根据子布局的大小而定,肯定是获取最大的那个子view的大小了
- 仿照一个linearlayout 的vertical 写一个简陋的 viewgroup

这样的。这里写图片描述

package wang.com;

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

/**
 * 创建日期: 16/3/16 上午11:16
 * 作者:wanghao
 * 描述:
 */
public class MyViewGroup extends ViewGroup {


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

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

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
//        这里确定吧子view的布局放在那里. 很好理解吧.一般简单的就是 横着放 竖着放 .
//        不断地更改每个子布局的位置 .
        int height = 0;
        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                MyLayoutParams ml = (MyLayoutParams) child.getLayoutParams();
                child.layout(//
                        getPaddingLeft() + ml.leftMargin,//
                        getPaddingTop() + ml.topMargin + height,//
                        getPaddingLeft() + ml.leftMargin + child.getMeasuredWidth(),//
                        getPaddingTop() + ml.topMargin + child.getMeasuredHeight() + height//
                );
                height += child.getMeasuredHeight() + ml.bottomMargin + ml.topMargin;

            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int parentWidth = 0;//父窗体根据子布局的大小变化 其实就是档wrap_content的时候
        int parentHeight = 0;
        int cacheWidth = 0;//缓存父宽度


        if (getChildCount() > 0) {
        //        直接获取第一个view的宽度 ,之后就和其他的view进行对比了
        View firstchild = getChildAt(0);
        MyLayoutParams firstml = (MyLayoutParams) firstchild.getLayoutParams();
        cacheWidth = firstchild.getMeasuredWidth() + firstml.leftMargin + firstml.rightMargin;

            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
//                必须要自定义 然后继承MarginLayoutParams 才能获取margin ,不然空指针
                MyLayoutParams ml = (MyLayoutParams) child.getLayoutParams();
                measureChildren(widthMeasureSpec, heightMeasureSpec);


                //我们想获取竖着的 宽度最大值的那个view的宽度作为 父窗体的宽度  (这么绕嘴呢)
                parentWidth = Math.max(cacheWidth, child.getMeasuredWidth() + ml.leftMargin + ml.rightMargin);
                cacheWidth = parentWidth;//把最大值赋值给缓存 在进行和下一个的比较
                parentHeight += child.getMeasuredHeight() + ml.topMargin + ml.bottomMargin;

            }
            //获取padding边距
            parentWidth += getPaddingLeft() + getPaddingRight();
            parentHeight += getPaddingTop() + getPaddingBottom();
        }
        // 设置最终测量值O
        setMeasuredDimension(resolveSize(parentWidth, widthMeasureSpec), resolveSize(parentHeight, heightMeasureSpec));
//        resolveSize(int,int);这个方法不要怕,就是 在 两个int值里面选择一个 作为最后只 赋值给viewgroup
//        总之就是:在AT_MOST模式下 从xml里面获取的值 和viewgroup获取子view占的面积的值 选择最小的 
    }

    /*———————————————如果想获取margin 所有自定义viewgroup都要自定义这个,可以参考LinearLayout,FrameLayout等 ———————————————————*/
    public static class MyLayoutParams extends MarginLayoutParams {
        public MyLayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public MyLayoutParams(android.view.ViewGroup.LayoutParams source) {
            super(source);
        }

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

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

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

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

    @Override
    public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

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

}

这里面用到了几个 方法 简单说下:

  • getChildCount() :获取几个子view的值
  • getChildAt(i):获取 某个子view

  • resolveSize(int ,int )

 /**
     * Version of {@link #resolveSizeAndState(int, int, int)}
     * returning only the {@link #MEASURED_SIZE_MASK} bits of the result.
     */
    public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
    }
    在进去看看呢
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST: 当viewgroup时wrap_content,match_parent的时候
                if (specSize < size) {//选择了最小的那个值。
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }

                break;
            case MeasureSpec.EXACTLY://精确的是 就显示 精确的值
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED://不用管 
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
    最后感觉自定义viewgroup还是比较复杂的。考虑的因素比较复杂的

如何获取控件的大小
git上的demo,欢迎start fork

参考《android开发艺术探索》,《android群英传》。
自重原创:http://blog.csdn.net/wanghao200906/article/details/50906799

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐