View 自定义控件 测量 布局 绘制

View 是控件的基础,不论是系统的控件,还是自定义控件,都需要遵循View的规则,都是View的子类,我们知道控件有 大小 布局 绘制图案 等,对应的是view 的 onMeasure()  onLayout() 和 onDraw() 三个方法,咱们重点说说 测量大小 这一部分。

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

这个就是View里面测量宽高的方法,我们知道view是依附于PhoneWindow的,假设它的依附的Window的宽和高都是固定的,实际上也是固定的,一般就是手机屏幕的宽和高;我们在它的基础上,把View 放到它上面。我们看看方法中两个形参,一个代表宽,一个代表高,这两个参数值是怎么来的呢? 我们知道,一般使用View时,需要在xml布局中声明,或者直接new 一个出来,
        <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

        View view = new ImageView(Context);
        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        view.setLayoutParams(layoutParams);

上述两个方法其实意思是一样的。用的方法都是铺满全屏,我们发现,里面调用的是 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {}方法,而它又调用了
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
方法,也就是说, getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec) 方法就是决定宽的方法,我们继续看,
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
这个意思是说,如果没有背景图,取我们设置的最小宽;如果有背景图,取背景图的最小宽和设置的最小宽值中比较大的一个,知道这个方法后,我们继续看
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
此时,两个形参,int size, int measureSpec,size是最小宽,measureSpec 这个值是系统根据window计算出来的,对应的是 xml布局中的 android:layout_width="match_parent" 和代码中的 ViewGroup.LayoutParams.MATCH_PARENT ,在这里补充点东西,measureSpec 是二进制形式标识,一共32位,前面两位记录的是view的模式,后面30位记录的是数值。我们可以看一下辅助静态类 MeasureSpec , View 的模式分为三种 UNSPECIFIED、 EXACTLY、 AT_MOST, 这三种的意思很明确,代表不确定值、明确的值、不超过父类布局的值,例如 ListView  Scroll 的高,用的一般都是 UNSPECIFIED 这种模式,即只要需要,多少都行; 我们在xml中写到 android:layout_width="10dp", android:layout_width="match_parent",这种具体的值,以及铺满父类容器,由于父类是一般是固定的,所以这种都是有具体值的; android:layout_width="wrap_content" 这种就是不超过父类,自己够用就行,属于不确定的。具体的可以参考一下文献,这三个值不在本章的重点。

继续看getDefaultSize()方法,进来后,把最小值作为初始值,然后通过 MeasureSpec 方法获取到view的宽的模式和大小,如果是无限制模式,则赋值为最小的宽;如果是其他两种模式,则选取系统测量的宽度;我们可以理解为这是个view的基本默认测量模式。
举个例子,比如我们 LinearLayout 为xml根节点,
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
            <View
                android:layout_width="wrap_content"
                android:layout_height="20dp"
                android:minWidth="50dp"
                android:background="#111111"
                />
    
            <View
                android:layout_width="match_parent"
                android:layout_height="20dp"
                android:minWidth="50dp"
                android:background="#111111"
                android:layout_marginTop="10dp"
            />
        </LinearLayout>

我们会发现,两个view的背景是黑色的,它的宽铺满了全屏,虽然他是自适应的或撑满父容器,但根据getDefaultSize() 方法,它的宽就是系统给它的值,是依赖于父类容器,也就是屏幕的宽;可能会有异议了,比如上面的view,我们的宽是自适应内容,并且最小宽是50dp,我们就像让它的宽为50dp,第二个view的宽继续与父容器一样,怎么办?我们可以自定义控件,修改getDefaultSize() 方法,修改 case MeasureSpec.AT_MOST: 里面的代码,做一个比较,specSize 和 size 做比较,取较小的值即可,顺着这个思路,发现View里面已经有了现成的方法

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

我们可以看的出,这个方法 childMeasuredState 为 0,则用 & 运算后值也为0, 再用 | 运算其他数字,则数字值无影响。这个方法比着上面的方法,差别就在 case MeasureSpec.AT_MOST中,我们看见,在此做了个判断,如果传进来的值比系统给的值小,则用传进来的值,同时进行位运算,MEASURED_STATE_TOO_SMALL 做出了比较小的标识。我们可以自定义控件试试效果

public class TestView1 extends View {


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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
}

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
            <com.TestView1
                android:layout_width="wrap_content"
                android:layout_height="20dp"
                android:minWidth="50dp"
                android:background="#111111"
                />
    
            <com.TestView1
                android:layout_width="match_parent"
                android:layout_height="20dp"
                android:minWidth="50dp"
                android:background="#111111"
                android:layout_marginTop="10dp"
            />
        </LinearLayout>
我们再看一下效果,发现,上面的view显示的效果确实是宽没有铺满父容器,而是显示的50dp,下面的view是铺满父容器。这两个方法,大家可以理解不同之处了。比如我们要自定义一个星星评级,每个星星宽为10dp,我们想让view的宽度随着星星个数增多而变大,此时我们在重写onMeasure()方法时,就要计算出n个星星的宽度是多少,然后把算出的值,按照上面自定义view中的写法,作为resolveSize()方法第一个参数传进去,同时xml布局中,宽度设置为 android:layout_width="wrap_content" ,宽的值就确定了。

关于控件View测量的部分,大概就是这样了。布局这一部分,一般是ViewGroup会用到,View之类的一般用不到,这个方法是个空方法,忽略即可。
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

    }
View自定义第三部,是绘制。我们需要重写方法 onDraw(),
    protected void onDraw(Canvas canvas) {
    
    }

我们知道,draw过程是将View绘制到屏幕上,它遵循下面几步:绘制背景 background.draw(canvas);绘制自己 onDraw;绘制children(dispathDraw);绘制装饰(onDrawScrollBars),View的话,只执行绘制自己即可,也就是上面的 onDraw() 方法。这里介绍两个类:Paint 和 Canvas。打个比方,Paint像画笔,可以调整画笔的颜色,画出线的宽度;Canvas 是画布,需要Paint在它上面绘画,Canvas 本身提供一些画点、线、圆、弧线、直线、扇形、矩形、Bitmap、图片、文字等的方法,还有置转换的方法rorate、scale、translate、skew(扭曲)等。我们可以根据提供的api,画出自己想画的图形。比如说柱状图、饼状图、评分星星控件等,都是画出来的。
 

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/88560620