自定义View的流程:Measure

自定义View主要分measure,layout,draw三大步骤,对于直接继承View,则只需要完成measure和draw,对于直接继承ViewGroup的自定义view,则需要完成measure,layout,draw这三个步骤。view的绘制流程是重ViewRoot的performTraversals方法开始的,performTraversals方法会依次调用performMeasure方法,performLayout,performDraw方法来完成一个view的绘制。performMeasure方法中,会调用measure方法,measure方法完成对自身的测量,接着又回调用onMeasure方法,onMeasure方法内部,又回调用各个子view的measure方法完成对子view的测量,子view在重复这个过程,完成对整个view树的测量工作。performLayout和performDraw的过程和performMeasure的过程是类似的,只不过,绘制子view时,调用的是dispatchDraw方法。这就是view绘制的整体流程。整体流程知道了,我们还需要了解各个步骤的细节,才能写出自定义控件。下面开始了解一下,measure这个步骤的具体细节。
Measure过程:

    /**
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	   //具体代码省略
        ...
	   onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

首先,我们查看View的源码的measure方法,该方法是final修饰的,表示该方法不能被重写,可以看到该方法需要传递两个参数,widthMeasureSpec和 heightMeasureSpec,通过这个方法的参数注释,可以知道这两个参数,是被父容器给确定的。为了方便理解后续的流程,我们先要了解什么widthMeasureSpec是个什么东西?简单的说,它就是测量规格,它是32位的int值,高两位代表了测量模式(specMode),低30位代表了测量的大小(specSize)。可以通过以下方法获取到测量模式和测量的大小
int specMode = MeasureSpec.getMode(measureSpec);//测量模式
int specSize = MeasureSpec.getSize(measureSpec);//测量大小
android系统一共提供了三种测量模式,分别是:
MeasureSpec.UNSPECIFIED
这种测量模式表示,控件想要多大就要多大,父控件对子控件不做限制,一般用于系统内部

MeasureSpec.AT_MOST
这种测量模式表示,父容器已经指定了一个specSize,子控件的最大宽度/高度不能超过这个specSize,但是,view的具体大小,要看view的具体实现

MeasureSpec.EXACTLY
这种测量模式表示,父容器已经检测出了View的大小,这个大小就是specSize
接着看onMeasure方法

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

这个方法内部看起来很简答,也只是调用了setMeasuredDimension(int measuredWidth,int measuredHeight)方法,这个方法的作用就是将测量的宽和高设置给view。这个方法的传入的参数,是通过getDefaultSize(int size, int measureSpec)方法来获取到测量的宽高的,下面看看getDefaultSize(int size, int measureSpec)是如何获取到测量的宽和高的。

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

getDefaultSize方法内部的代码知道,这个方法返回的测量大小,是根据测量模式决定的,如果测量模式是AT_MOST或者EXACTLY,则返回的测量大小就是specSize,如果测量模式是UNSPECIFIED,则测量的大小就是传入的size。那这个传入的size究竟是多大呢?这个size的大小是getSuggestedMinimumWidth()方法获取的,下面我们来看看getSuggestedMinimumWidth()方法的源码

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

通过getSuggestedMinimumWidth()方法内部的代码可以知道,如果view的background为null,则这个方法返回的大小就是mMinWidth,这mMinWidth其实就是View的属性android:minWidth设置的大小,如果View未设置这个minWidth属性,则mMinWidth的默认大小就是0,如果view的background不为null,则会执行max(mMinWidth, mBackground.getMinimumWidth())也就是取mMinWidth和mBackground.getMinimumWidth()两者中的较大值。我们在看看mBackground.getMinimumWidth()方法,这个方法是Drawable类中的方法,getMinimumWidth()方法的源码如下:

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

通过查看getMinimumWidth()方法的源码发现,如果这个background是个BitmapDrawable,则返回这个BitmapDrawable的大小,如果是ShapeDrawable,则返回是0。经过对setMeasuredDimension方法的分析,我们知道,view的测量宽度/高度,就是传入的measurSpec这个测量模式中的specSize。为什么这样说呢,因为一般我们自定义的view,测量模式不会是UNSPECIFIED的,所以,在getDefaultSize方法中,也就不会执行UNSPECIFIED这个case分支,所以getDefaultSize返回的就是specSize。这个specSize就是view的测量宽高,注意是
测量宽高,不是最终宽高,因为最终宽高是在layout方法中才确定的。这就是view的测量过程。分析完这个过程,有些人可能还有疑问,父容器是如何计算view的测量模式的呢?这种情况还只是view存在父容器的时候,如果对于DecorView,这样的顶层view,它是没有父容器的,它的MeasureSpec是如何计算的呢?我们先来看DecorView的MeasureSpec是如何计算的,对于,DecorView来说,它的具体实现类ViewRootImpl中的measureHierarchy方法中有如下一段代码:

   childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.widht);
   childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);
   performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);

接下来,看看getRootMeasureSpec方法的实现:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

从getRootMeasureSpec方法中,可以得出结论,DecorView的测量模式,只有两种,一种是EXACTLY,一种是AT_MOST,当DecorView的layoutParams是具体的数字或者MATCH_PARENT时,它的测量模式就是EXACTLY,否则就是AT_MOST,并且,当DecorView的宽度是具体的数字时,它的specSize就是这个具体的数字,否则,它的specSize就是window的宽或者高。其实通过getRootMeasureSpec方法,可以确认,DecorView就是走的第一个case。它的测量模式就是EXACTLY,specSize就是window的宽度或者高度。看完了DecorView的测量模式是如何计算的后,在来看看普通的view的测量模式是如何计算的。对于普通的view,也就是我们的Activity显示的布局的根View是一个ViewGroup,我们可以查看ViewGroup类中的measureChildWithMargins()方法,这个方法的具体实现是:

  protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

通过measureChildWithMargins方法,我们可以发现,子view进行测量前,会先计算子view的measureSpec,具体的计算过程是在getChildMeasureSpec()这个方法中完成的,下面我们来看看ViewGroup类中的getChildMeasureSpec()这个方法的具体实现:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

通过对getChildMeasureSpec()代码的分析,发现,view的measureSpec是由父容器的messureSpec和view的layoutParms共同决定的。并且可以得出以下结论:
1.当view的宽或高采用具体数字,比如100px或者100dp时,无论父容器的测量模式是什么,view的specMode都是EXACTLY的,并且view的大小就是这个具体的数字。

2.当view的宽或高采用MATCH_PARENT时,view的specMode和父容器的测量模式保持一致。但是specSize的值,确不同,当父容器的SpecMode是UNSPECIFIED时,view的specSize是0,当父容器的SpecMode是EXACTLY或者AT_MOST时,view的specSize的大小就是父容器的剩余空间。

3.当view的宽或高采用WRAP_CONTENT时,如果父容器的SpecMode是EXACTLY或者AT_MOST时,view的测量模式就是AT_MOST,当父容器的specMode是UNSPECIFIED时,view的测量模式也是UNSPECIFIED,view的测量大小也受父容器的测量模式的影响,具体是当父容器的SpecMode是EXACTLY或者AT_MOST时,view的specSize的大小最大不能超过父容器的剩余空间。父容器的specMode是UNSPECIFIED时,view的测量大小就是0 。

根据getChildMeasureSpec方法的逻辑,可以总结出view的测量模式和父容器的测量模式以及view自身的layoutParams之间的关系图如下:
在这里插入图片描述
通过上图,就可以很好的解释一个现象,当我们自定义一个CustomView,直接继承View时,如果CustomView的layoutParams是WRAP_CONTENT时,如果不给CustomView设定一个默认的宽和高,则在使用CustomView时,无论CustomView的宽或者高设置成MATCH_PARENT或者WRAP_CONTENT时,CustomView的大小始终是一样的,都是父控件的剩余空间,也就是上图中的parentSize,也就是说给CustomView的宽高设置成MATCH_PARENT和WRAP_CONTENT是CustomView的大小是一样的,这就和我们平时使用系统控件时给系统控件设置成MATCH_PARENT和WRAP_CONTENT看到的效果是不一样的。这样就违背了我们的主观逻辑。为了解决这个问题,我们需要 在CustomView的layoutParams是WRAP_CONTENT时,给CustomImageview设置默认的宽高。
上面这个过程完成了View的measure过程分析,具体ViewGroup的测量是如何进行的呢?其实ViewGroup是个抽象类,它没有重写父类View的onMeasure方法,而是在自己内部定一个了measureChildren方法,通过该方法完成对子view的测量。下面来看看measureChildren方法的具体实现:

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

这个方法内部其实,就是遍历自己的子view,然后判断子view的Visibility是否是GONE,如果不是的,就
调用measureChild对各个子view进行测量。下面来看看measureChild方法的具体实现:

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

measureChild方法的实现过程就很明确了,就是根据子view的layoutParmas和父容器的MeasureSpec,通过
getChildMeasureSpec来计算出子view的MeasureSpec,在将计算的MeasureSpec传入到view的measure方法中,完成对view的测量工作了,后续的具体测量过程,前面已经分析过了,这里就不再重复这个过程了。


前面提到,ViewGroup是个抽象类,它并没有实现onMeasure方法,为什么ViewGroup方法没实现onMeausre方法呢?这是因为,每个具体的ViewGroup的子类,他们的特性都是不同的,这就决定了他们的测量方式就是不同的,比如,LinearLayout,它在Vertical方法和Horizontal方法的测量方式就是不同的,需要不同的实现,RelativeLayout的特性和LinerLayout也是不同的,导致RelativeLayout的测量方式和LinearLayout的测量方式也是不同的,这样LinearLayout
,RelativeLayout的父类ViewGroup中的onMeasure就不能有具体实现,只能是它的各个子类自己去按照自己的布局特性去自己实现。

有时候我们想要在Activity中获取view的宽和高,有些人可能直接就在Activity的生命周期方法中去调用view的getWidth()或者getMeasuredWidth()方法去获取,这种获取view的宽高的方式其实并不能获取到view准确的宽高,这是因为,Activity的生命周期和view的测量过程并不是同步的,这就导致,在Activity的生命周期中获取view的宽高时,view还并未测量或者还未测量完毕,这时获取到的view的宽高是不准确的。想要获取的view的准确宽高,可以通过如下几种方式:
1.通过view获取到ViewTreeObserver,然后给ViewTreeObserver添加onGlobalLayoutlisnrter监听,当View树内部的view的可见性发生改变时,就会回调onGlobalLayout方法,这时,就可以在这个方法中获取view的宽高了。具体获取view宽高的kotlin代码如下:

        tv.viewTreeObserver.addOnGlobalLayoutListener (object:ViewTreeObserver.OnGlobalLayoutListener{
            @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
            override fun onGlobalLayout() {
                tv.viewTreeObserver.removeOnGlobalLayoutListener(this)
                LogUtil.i("${tv.width }        ${tv.measuredWidth}")
            }

        } )

2.通过view的post方法,将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化完毕,这时获取到的view的宽高就是准确的。具体代码实现:

        val task = Runnable {
            LogUtil.i("${tv.width}     ${tv.measuredWidth}")
        }
        tv.post(task)

3.在Activity的onWindowFocusChanged方法中,获取view的宽高,具体代码实现:

override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if(hasFocus){
            LogUtil.i("${tv.width}     ${tv.measuredWidth}")
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值