美文网首页
Measure过程

Measure过程

作者: Utte | 来源:发表于2018-08-07 22:00 被阅读24次

一、MeasureSpec

MeasureSpec是View的一个静态内部类。他是测量过程的依据,所以先要了解这个类。它是一个32位int型的量,高2位表示SpecMode,低30位表示SpecSize。

1. SpecMode分类

  • UNSPECIFIED(00):父容器不对View做任何限制。
  • EXACTLY(01):精确大小,测量大小为SpecSize。对应具体数值或match_parent。
  • AT_MOST(10):父容器指定最大值。对应wrap_content。

2. MeasureSpec类

public static class MeasureSpec {
    // mode需要的偏移量
    private static final int MODE_SHIFT = 30;
    // 11 000000000000000000000000000000
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    public @interface MeasureSpecMode {}
    // 三种SpecMode对应的值
    // 00 00...0
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    // 01 00...0
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    // 10 00...0
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    // 合并SpecMode和SpecSize生成MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    // 加判断UNSPECIFIED
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
    // 从MeasureSpec中提取SpecMode
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    // 从MeasureSpec中提取SpecMode
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    // 调整MeasureSpec中的SpecSize
    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        // UNSPECIFIED不需要调整
        if (mode == UNSPECIFIED) {
            return makeMeasureSpec(size, UNSPECIFIED);
        }
        // 调整SpecSize
        size += delta;
        if (size < 0) {
            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                    ") spec: " + toString(measureSpec) + " delta: " + delta);
            size = 0;
        }
        // 重新打包
        return makeMeasureSpec(size, mode);
    }
    
    public static String toString(int measureSpec) {
        // ...
    }
}

3. MeasureSpec的创建依据

  • 对于DecorView的确定因素
    • 窗口尺寸
    • 子身的LayoutParams
  • 对于普通View的确定因素
    • 父布局的MeasureSpec
    • 自身的LayoutParams

一个View的MeasureSpec确定后,View的宽高会在onMeasure()根据MeasureSpec来确定。

4. DecorView的MeasureSpec创建

View的绘制流程是从ViewRootImpl的performTraversals()开始的,其中会调用measureHierarchy()创建DecorView的MeasureSpec。

ViewRootImpl # measureHierarchy()
// ......
// desiredWindowWidth为窗口的宽度,lp.width为DecorView的layourParams中设置的宽度
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
// 得到DecorView宽高的MeasureSpec后调用performMeasure()去完成DecorView的测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// ......
ViewRootImpl # getRootMeasureSpec()

根据window尺寸和DecorView的LayourParams来决定DecorView的MeasureSpec。

// windowSize就是Window的大小,rootDimension指的是DecorView的layoutParams中设置的大小
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    // match_parent和fill_parent的值都是-1
    case ViewGroup.LayoutParams.MATCH_PARENT: windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    // DecorView设置的具体数值
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}
ViewRootImpl # performMeasure()

传入创建好的MeasureSpec,调用View的measure()开始测量过程。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        // 开始测量过程
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}
总结

DecorView的MeasureSpec由自身LayoutParams和Window尺寸决定。

layoutParams\MeasureSpec SpecSize SpecMode
MATCH_PARENT windowSize EXACTLY
WRAP_CONTENT windowSize AT_MOST
具体数值 rootDimension EXACTLY

5. 普通View的MeasureSpec创建

普通View的measure()是由ViewGroup的measureChildWithMargins()调用的。

ViewGroup # measureChildWithMargins()
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 参数中的两个MeasureSpec是ViewGroup的MeasureSpec
    // widthUsed和heightUsed已经占用的宽度和高度
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 传入ViewGroup的MeasureSpec、ViewGroup中不属于自己的大小、View的layoutParams中定义的大小。
    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);
    // 计算出MeasureSpec后开始进行测量流程。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup # getChildMeasureSpec()
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 父布局的Mode和Size
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    // 父布局原大小减去已用大小如果小于0,就取0
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    // 如果父布局Mode是EXACTLY
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            // 自身设置的是具体数值
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 自身设置的是match_parent
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 自身设置的是wrap_content
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // 父布局Mode为AT_MOST
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // 自身设置的是具体数值
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 自身设置的是match_parent
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 自身设置的是wrap_content
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // 自身设置的是具体数值
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 自身设置的是match_parent
            // 如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 自身设置的是wrap_content
            // 如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    // 合并返回
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
总结

普通View的MeasureSpec由父布局的MeasureSpec和自身的LayoutParams决定。

自身lp\父布局mode EXACTLY AT_MOST UNSPECIFIED
具体数值 childSize EXACTLY childSize EXACTLY childSize EXACTLY
match_parent parentSize EXACTLY parentSize AT_MOST 0 or parentSize UNSPECIFIED
wrap_content parentSize AT_MOST parentSize AT_MOST 0 or parentSize UNSPECIFIED

二、View的measure过程

在measure()之前,会确定View的MeasureSpec,在前面已经说了。

1. View # measure()

  • ViewGroup的measureChild()调用measure()
  • 是测量过程的入口
  • 是一个fianl的方法
  • 内部会调用onMeasure()
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // ......
    // 内部会调用onMeasure(),自定义View可以去重写这个方法来满足我们的自定义测量需求。
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    // ......
}

2. View # onMeasure()

  • 在自定义View时通常会重写该方法以满足特定的需求
  • 首先会获取期望默认最小值
  • 根据默认值和specSize来确定测量宽高
  • 保存计算完成的测量宽高
// 根据MeasureSpec测量View的宽高并保存到mMeasureWidth/Height
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

3. View # getSuggestedMinimumWidth/Height()

获得默认宽高。

// 这里获取到了建议最小宽度,判断了该View是否含有背景图
// 如果没有,就是全局mMinWidth
// 如果设置了,就是mMinWidth和背景图片的原始宽度的大者
// 并不是所有图片都有原始宽度。
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
// Drawable # getMinimumWidth()
public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
// Drawable # getIntrinsicWidth()
// Drawable的 getIntrinsicHeight()默认返回-1,可以重写。
// 比如BitmapDrawable重写了,ShapeDrawable没有。
public int getIntrinsicWidth() {
    return -1;
}

4. View # getDefalutSize()

// 根据最小值和MeasureSpec来确定测量宽高
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    // 如果是UNSPECIFIED,测量大小为建议大小。
    // AT_MOST或EXACTLY,测量大小为MeasureSpec中计算好的大小。
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

5. wrap_content和自定义的问题

会发现如果按照View的onMeasure()的默认实现,设置match_parent和wrap_content的效果是一样的。因为两种模式下的specSize都是parentSize(见前面分析View的MeasureSpec的生成),而getDefaultSize()中的最终测量宽高取得就是specSize,所以两种情况下都是match_parent的效果。所以在自定义View的时候如果需要对wrap_content做处理。

对于TextView、Button、ImageView等,它们的onMeasure方法系统了都做了重写,不会这么简单直接拿 specSize来当大小,而去会先去测量字符或者图片的高度等,然后拿到View本身content这个高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做为View的大小。

6. View的Measure过程总结

  • 从measureChildWithMargins()开始,计算View的MeasureSpec。
  • measureChildWithMargins()调用measure,开始测量过程。
  • measure()调用onMeasure()。
  • onMeasure()中确定测量宽高。
    • 根据背景图的最小尺寸和全局mMinWidth/Height确定默认值。
    • 根据specMode来决定最终尺寸是取默认值还是specSize。
  • onMeasure()确定测量尺寸后设置给全局变量。

三、ViewGroup的measure过程

因为不同ViewGroup的布局相差太多,无法做统一实现,所以不像View一样,ViewGroup没有实现onMeasure,但是提供了measureChildren()向下分发测量过程。

1. ViewGroup # measureChildren()

会遍历子View调用measureChild()。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    // 遍历子View
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        // 如果这个子View不为GONE,就调用measureChild()去测量
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

2. ViewGroup # measureChild()

  • 调用到了前面提到的计算MeasureSpec的getChildMeaureSpec()。
  • 调用子View的measure向下递归测量。
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    // 计算MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    // 开始子View的测量流程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

三、测量宽高的获取

1. 注意点

  • onMeasure()中获取的测量宽高可能不准确,因为某些情况下,会多次measure才能确定最终的测量宽高。
  • 在Actvity的onCreate()、onStart()、onResume()中获取有可能会获取不到,因为View的测量和Activity的生命周期不是同步的。在没有测量完毕时获取到的宽高为0。

2. 获取方式

测量结束后,可以通过getMeasuredWidth/Height()获取。这里的重点是确定合适测量结束。

3. 测量结束的时机

Activity/View # onWindowFocusChanged

这个方法会在View初始化完毕后Activity得到焦点或失去焦点时被调用。当频繁进行onResume()和onPause()时,这个方法就会频繁地被调用。

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

performTraversals也是通过向消息队列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();
        }
    });
}
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();
        }
    });
}
手动measure

可以手动对View进行measure来获得View的测量宽高,需要根据View的LayoutParams来分。

  1. match_parent

这种情况无法使用手动mesaure来实现,因为根据MeasureSpec的创建,当自身lp为match_parent时,specSize为parentSize,这种方法无法获取parentSize,所以不可行。

  1. 具体数值
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);
  1. 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);

相关文章

网友评论

      本文标题:Measure过程

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