美文网首页
View工作原理之measure

View工作原理之measure

作者: 李die喋 | 来源:发表于2019-07-29 21:22 被阅读0次

MeasureSpec

基本概念

MeasureSpec参与了View的measure过程,系统根据MeasureSpec来测量出View的测量宽/高

  • 对于DecorView,其MeasureSpec是由窗口尺寸的大小和自身的LayoutParams来共同确定的
  • 对于普通的View,是由父容器的MeasureSpec和自身的LayoutParams共同决定的

一个MeasureSpec由MeasureMode和MeasureSize组成,分别指测量模式和规格大小。

public static class MeasureSpec {
    //偏移量
    private static final int MODE_SHIFT = 30;
    //1100 0000 0000 0000 0000 0000 0000 0000
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    //合并SpecSize和SpecMode生成MeasureSpec
    public static int makeMeasureSpec(int size,int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    
    public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
    }
    
    public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
    }
}

从上面的源码可以看到MeasureSpec是一个32位int值,高两位代表SpecMode,低30位代表SpecSize。SpecMode有三种模式:

  • UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize指定的值。他对应于LayoutParams中的match_parent和具体的数值这两种情况。
  • AT_MOST:父容器指定了一个可用大小,View的大小不能超过父容器的SpecSize。具体的值要看不同View的具体实现,它对应于LayoutParams中的wrap_content。

DecorView的MeasureSpec创建过程

在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

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

其中传入的参数desiredWindowWidth、desiredWindowHeight是屏幕的宽度和高度,接着再看下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;
}

根据LayoutParams中的宽/高的参数来划分:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
  • LayoutParams.WTAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  • 固定大小:精确模式,大小为LayoutParams中指定的大小。

普通View的MeasureSpec创建过程

这里的View指的是我们布局中的View,View的measure过程由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);
}

从上面的代码看出,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关。得到子元素MeasureSpec的具体情况可以看下getChildMeasureSpec()方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //获取子view的真实宽度
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 若当前view的SpecMode是EXACTLY
    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) {
           //如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
            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;
    }
    //合并得到MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述代码主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。

child布局参数/父元素Mode EXACTLY AT_MOST USPECIFIED
dp\px EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

总结

  • Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象与DecorView创建联系
  • View的绘制流程从ViewRootImpl的PerformTraversals()方法开始。performTraversals()先计算出窗口的大小,再通过getRootMeasure()方法,计算出DecorView的宽高,传入performMeasure()方法中
private void performTraversals() {
  ...
  if (!mStopped) {
    boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
            (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
    if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
            || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
        
        // 获取测量规格,mWidth 和 mHeight 当前视图 frame 的大小
        // lp是WindowManager.LayoutParams
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

         // Ask host how big it wants to be
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
      }
      ...
    }
}
  • 开始view的measure,起作用的其实是onMeasure()方法

View的measure过程

View的measure方法中回去调用View的onMeasure方法,如下所示:

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

setMeasuredDimension()会设置View宽高的测量值

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

当AT_MOST和EXACTLY这两种情况时,返回的值都是measureSpec的specSize,specSize是测量后的大小。

UNSPECIFIED这种情况,一般用于系统内部的测量,在这种情况下,View的大小为getDefaultSize的第一个参数size,宽高分别为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。

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

 protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

这里判断了View是否设置了背景,若设置了背景,View的宽度为mMinWidth。mMinWidth对应于android:minWidth这个属性所指的值,若不设置默认为0。如果View设置了背景,则View宽度为max(mMinHeight, mBackground.getMinimumHeight()):

public int getMinimumHeight() {
    final int intrinsicHeight = getIntrinsicHeight();
    return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
//返回drawable内在高度,如果drawable没有内在高度返回-1
public int getIntrinsicHeight() {
        return -1;
}

根据上述代码,得到getMinimumHeight()返回的就是drawable的原始高度,前提是drawable有原始高度,没有就返回0。例如:ShapeDrawable无原始宽/高,BitmapDrawable有原始宽/高。

wrap_content和自定义问题

从getDefaultSize()方法来看,若按照onMeasure的默认方法实现,当宽/高设置成wrap_content或match_parent时,最后的结果都是specSize,也就是两种情况下都是match_parent的效果。所以在自定义view下的时候需要对wrap_content进行处理。

在自定义view的时候,我们通过判定宽高是否是wrap_content来给宽/高是wrap_content的情况设定一个默认的内部宽/高。像TextView、ImageView都对这里做了处理。

View的Measure过程总结

  • 从ViewGroup#measureChildWidthMargins开始,计算View的MeasureSpec
  • measureChildWidthMargins()调用child.measure()开始测量过程
  • View#measure()调用onMeasure()
  • onMeasure()方法测量View宽高
    • 根据是否有背景图来确定View的默认大小
    • 根据SpecMode来确定View测量后的大小是SpecSize还是默认大小
  • onMeasure()测量后的数据给全局变量mMeasuredWidth和mMeasuredHeight

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个过程。

ViewGroup#measureChildren

遍历子view调用measureChild()

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];
        //当child不是GONE时,就去测量
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

ViewGroup#measureChild()

  • 前面提到过的measureChildWithMargins()中的getChildMeasureSpec(),开始测量
  • 调用子view向下递归测量
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);
}

ViewGroup没有定义测量的具体过程,因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现。

获得View宽高的四种方法

在实际操作中我们可能回去获取某个view的宽和高,但是在onCreate() onResume()方法中一般是获取不到的,因为View的measure过程和Activity生命周期方法不是同步执行的。

1.Activity/View#onWindowFocusChanged

onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了。当Activity窗口得到焦点和失去焦点是均会被调用一次,当频繁进行onPause和onPause,会被频繁调用。

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

2.view.post(runnable)

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了

@Override
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

3.ViewTreeObserver

可以通过ViewTreeObserver添加监听来监听View的各种动态。onGlobalLayout()会在View树发生改变或是View树内部的View的可见性发生改变时调用。

@Override
protected void onStart() {
    super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
        @Override
        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

4.手动measure(view.measure(int widthMeasureSpec,int heightMeasureSpec))

对view进行measure得到view的宽 高,需要根据view的LayoutParams来分:

match_parent

无法measure出具体宽高。因为要测量出当前view尺寸,需要知道父容器的属于空间,无法知道parentSize。

wrap_content

((1 << 30) - 1)是specMode支持的最大值

int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);

具体数值

LayoutParams lp = view.getLayoutParams();
int widthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);

相关文章

网友评论

      本文标题:View工作原理之measure

      本文链接:https://www.haomeiwen.com/subject/gessrctx.html