android view 原理 -- measure 分析与应用

1 概述

measure方法,主要是用于测量android中view的大小,为后面的layout做好准备,这里我们主要来看measure的流程。

2 分析

查看view中的方法,

 public final void measure(int widthMeasureSpec, int heightMeasureSpec)

这个方法是测量方法,但是这里这个方法是final的,也就是说无法重写,其实这里面最终是调用onMeasure来完成测量。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

android的视图测量就是从上到下遍历的测量,他的流程如下:

android view 原理 -- measure 分析与应用_第1张图片

比较简单,值得注意的是measure方法中有一个widthMeasureSpec值,这个值是一个int,它的高两位代表了specMode,低30位代表了specsize,可以使用MeasureSpec来打包和解包这个值,从而获得mode和size。

先来看看MeasureSpec的mode:

UNSPECIFIED//不做限制
EXACTLY//指定精确值,在match_parent和具体数值时,适用与这种场景
AT_MOST//指定最大值,往往对应于layoutParams中的wrap_content

一个普通view的mode如何来确立呢,我们查看android中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;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这里的代码其实比较简单,总的来说,从参数来看,需要的参数是当前viewGroup的MeasureSpec以及padding,还有子view的大小,在里面判断的时候,还会结合子view的layoutParams。这里可以把上面的代码总结成一个表格:

可以看到,parent和子view的关系。

3 应用

(1) 自定义控件实现wrap_content

在自定义控件的时候,如果不重写onMeasure方法,往往wrap_content配置不起作用,这是为什么呢。我们先来看看View的onMeasure方法:

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

这里面调用了setMeasuredDimension方法来设置最终的大小,所以这里面决定大小的实际上是getDefaultSize方法,看看这个方法的实现:

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

这里我们先不管参数size,在AT_MOST和EXACTLY这两种模式下用不到,我们结合前面讲解的getChildMeasureSpec方法以及表格可以知道,当前view的MeasureSpec是根据parent和当前view的layoutParams决定的,例如wrap_content,这个在上面的表格中有所体现,可以看到,最终wrap_content和match_parent和parent的mode结合后,就会产生AT_MOST和EXACTLY这两种模式,且size都是parent的size,而上面的方法中,这两种模式的处理方式都是直接返回specSize,由于size是parent的size,也就导致了其实wrap_content和match_parent一致。

所以我们需要重写onMeasure方法来实现wrap_content

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            //宽高都是wrap_content
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            //宽是wrap_content
            setMeasuredDimension(mWidth, heightMeasureSpec);
        } else if (heightMeasureSpec == MeasureSpec.AT_MOST) {
            //高是wrap_content
            setMeasuredDimension(widthMeasureSpec, mHeight);
        }
    }

这里的mWidth和mHeight是自己根据情况自定的,以便实现wrap_content效果。

(2) 获取view宽高

要获取view的宽高,必须等待view测量完毕后才能得到正确的大小,那么如何保证获取的时候已经测量完毕,这里一共有四种方法

(a)onWindowFocusChanged

这个方法activity和view都有,在焦点发生变化的时候,就会回调这个方法。

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasFocus()) {
            //获取大小
        }
    }

(b)view.post(runnable)

我们知道,android中view的事件,包括touch,测量等等,都是通过发送消息到ui线程来执行的,那么使用这个方法,可以将我们获取大小的方法加入到队列末端。从而保证在测量完成后执行。

        post(new Runnable() {
            @Override
            public void run() {
                //获取大小
            }
        })

(c)ViewTreeObserver

这个类可以是一个view树的观察者,当它内部的view发生变化的时候就会回调其中响应的方法,这里使用OnGlobalLayoutListener,当view的layout状态发生变化的时候,就会回调。由于layout是发生在onmeasure之后的,所以保证了测量的完成。由于这个方法可能多次回调,所以在回调中,需要注销监听。

        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                //获取大小
            }
        });

(d)view.measure(int widthMeasureSpec, int heightMeasureSpec)

这种方法是直接调用view的measure方法来手动测量,但是这里要根据不同的layoutParams来做不同的方法。

match_parent

这种情况没办法测量,因为这里并不知道parent的大小。

具体数值

指定了具体数值,那么根据前面的表格,可以看到,mode就是EXACTLY,所以可以使用如下代码:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        view.measure(widthMeasureSpec,heightMeasureSpec);

这里的width和height就是指定的具体宽高。

wrap_content

同样的,根据前面的表格,这里的模式是AT_MOST,所以使用如下代码:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST);
        view.measure(widthMeasureSpec,heightMeasureSpec);

前面也讲到了,这里的widthMeasureSpec高两位是mode,低30是大小。所以必须左移30后减1,从而得到最大的值。

这篇文章介绍了measure的流程,以及应用,就到这里。

你可能感兴趣的:(android,布局,measure)