Android自定义控件入门到精通--View树的测量流程

《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683

《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code

文章目录

View树的测量流程

小故事:

公司计划搞团建,大狗,二狗,三狗是三个部门的领导,他们跑到财务室去跟财务妹子要经费。

财务妹子:你们各自要多少经费,提交申请给我就好了

大狗比较实诚,提交了500块的预算申请,财务妹子二话不说,直接给了大狗500块,大狗拿着500块就跟部门的人一起去吃酸辣粉了。

二狗又精又胆小,即怕要多了公司不高兴,又怕要少了底下的兄弟们不高兴。于是二狗跟财务妹子提了申请,写明:“按公司预算给经费!”,又是财务妹子也不兜着,把老板交代的单部门预算5000元都给了二狗,二狗高高兴兴的带着部门的人先去做了个大保健,又吃了个沙县小吃

三狗是公司得力干将,对地下兄弟们也比较好,胆子比较大,三狗给财务妹子提了个申请:“我也不知道兄弟们要多少经费,你先把公司预算给我,不够再说”,于是,财务妹子把5000块的预算给了三狗,三狗开开心心带着兄弟们又是大保健,又是海底捞,眼看预算都快花完了,这时候底下有个兄弟说,今天挺开心的,我们去K个歌吧,这时候三狗直接给财务妹子打了个电话:“经费不够用,再给我转一点”,财务妹子也没有办法,又给三狗转了1000块经费,三狗底下的兄弟是真能喝,没一会儿就把1000块经费干完了,三狗再次给财务打了电话,要求加经费,财务妹子又给三狗转了1000块钱,并回复三狗说:“这是公司最后底线,人要知足啊!”,三狗也知好歹,回复说道:“不会再要了”。

这个故事讲的就是View的三种测量模式以及ViewRoot的尺寸问题。

先了解下视图的加载流程

ViewManager->WindowManager->WindowManagerImpl-> WindowManagerGlobal -> ViewRootImpl

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}
//WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
   ......
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        //最终通过ViewRootImpl来发起view的测量、布局、绘图      
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        // do this last because it fires off messages to start doing things
        try {
            //将view设置给ViewRootImpl 
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}
//ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
        }
    }
}

上面的源码大家可以去看看,我们这里只要明白,我们的根view最终会被设置在ViewRootImpl对象中,然后在ViewRootImpl中实现测量、布局、绘图。

我们知道,在自定义View时,通过重写onMeasure()方法来测量计算自己需要的尺寸:

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

通过setMeasuredDimension()方法就可以定义自己View的尺寸,比如,我定义我们的自定义View为100x100尺寸大小:

setMeasuredDimension(100,100)

widthMeasureSpecheightMeasureSpec

两个是对测量模式和尺寸的封装,比如我们在xml布局文件中定义的layou_width和layout_height

来看个例子:

我们自定义了一个ChildView并设置它的宽高为50px

<cn.code.code.wiget.ChildView
    android:layout_width="50px"
    android:layout_height="50px"
    android:background="#ff2323" />

然后我们重写onMeasure方法并获取widthMeasureSpec、heightMeasureSpec的模式和size:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    Logger.e("MeasureSpec.UNSPECIFIED="+MeasureSpec.UNSPECIFIED);
    Logger.e("MeasureSpec.EXACTLY="+MeasureSpec.EXACTLY);
    Logger.e("MeasureSpec.AT_MOST="+MeasureSpec.AT_MOST);

    Logger.e(getClass().getSimpleName()+": onMeasure   widthMode="+(widthMode)+"   widthSize="+widthSize);
    Logger.e(getClass().getSimpleName()+": onMeasure   heightMode="+(heightMode)+"   heightSize="+heightSize);

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

日志输出:

05-21 15:20:46.950 4481-4481/? E/glacat: MeasureSpec.UNSPECIFIED=0
05-21 15:20:46.950 4481-4481/? E/glacat: MeasureSpec.EXACTLY=1073741824
05-21 15:20:46.950 4481-4481/? E/glacat: MeasureSpec.AT_MOST=-2147483648
05-21 15:20:46.950 4481-4481/? E/glacat: ChildView: onMeasure   widthMode=1073741824   widthSize=50
05-21 15:20:46.950 4481-4481/? E/glacat: ChildView: onMeasure   heightMode=1073741824   heightSize=50

可以看到,widthMeasureSpec和heightMeasureSpec就是我们在xml文件中设置的宽高属性。

我们的测量模式是EXACTLY,size=50px,说明layout_width/height给定确定数值时,它的测量模式是EXACTLY(跟大狗一样,明确知道自己要多少经费)

三种测量模式的理解:

  • UNSPECIFIED: Parent对Child没有尺寸约束,Child想要多大就给多大(这个我们一般不会用到)
  • EXACTLY: 代表尺寸是明确的,确定的数值 (match_parent和确定的数值)
  • AT_MOST: 代表Child也不知道自己需要多大尺寸,但是最大不会超过Parent的尺寸(wrap_content)

我们再来验证下我们的match_parent和wrap_content的模式和size

<cn.code.code.wiget.ChildView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff2323" />

onMeasure:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    Logger.e(getClass().getSimpleName()+": onMeasure   widthMode="+(widthMode>>30)+"   widthSize="+widthSize);
    Logger.e(getClass().getSimpleName()+": onMeasure   heightMode="+(heightMode>>30)+"   heightSize="+heightSize);

    //默认采用widthMeasureSpec和heightMeasureSpec的宽高
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

日志输出:

05-21 15:26:29.534 4581-4581/? E/glacat: MeasureSpec.UNSPECIFIED=0
05-21 15:26:29.534 4581-4581/? E/glacat: MeasureSpec.EXACTLY=1073741824
05-21 15:26:29.534 4581-4581/? E/glacat: MeasureSpec.AT_MOST=-2147483648
05-21 15:26:29.534 4581-4581/? E/glacat: ChildView: onMeasure   widthMode=1073741824   widthSize=720
05-21 15:26:29.534 4581-4581/? E/glacat: ChildView: onMeasure   heightMode=-2147483648   heightSize=1158
  • match_parent的mode=EXACTLY,size=屏幕宽度=720(跟二狗一样,预算多少就用多少)
  • wrapcont_parent的mode=AT_MOST ,size=屏幕高度=1158(跟三狗一样,不知道自己需要多少经费,先拿下全部预算经费再说)

那么对于View来说,它的尺寸是怎么确定的,是由谁决定的?是layout_width和layout_height吗?不一定!

例子:

在xml布局中,设置ChildView的宽为match_parent,高为20dp

<cn.code.code.wiget.ChildView
    android:layout_width="match_parent"
    android:layout_height="20dp"
    android:background="#ff2323" />

在onMeasure中,设置ChildView的宽高都为100

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    Logger.e("MeasureSpec.UNSPECIFIED="+MeasureSpec.UNSPECIFIED);
    Logger.e("MeasureSpec.EXACTLY="+MeasureSpec.EXACTLY);
    Logger.e("MeasureSpec.AT_MOST="+MeasureSpec.AT_MOST);

    Logger.e(getClass().getSimpleName()+": onMeasure   widthMode="+(widthMode)+"   widthSize="+widthSize);
    Logger.e(getClass().getSimpleName()+": onMeasure   heightMode="+(heightMode)+"   heightSize="+heightSize);
     //设置尺寸为100x100
    setMeasuredDimension(100,100);
}

效果:

在这里插入图片描述

日志输出:

06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: MeasureSpec.UNSPECIFIED=0
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: MeasureSpec.EXACTLY=1073741824
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: MeasureSpec.AT_MOST=-2147483648
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: ChildView: onMeasure   widthMode=1073741824   widthSize=720
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: ChildView: onMeasure   heightMode=1073741824   heightSize=30

widthSize=720,heightSize=30就是我们在布局文件中定义的宽高

但是最终,我们并没有把这个宽高设置给ChildView,而是通过

setMeasuredDimension(100,100);

把ChildView的宽高设置为了100x100

结论:View的宽高是由setMeasuredDimension()方法决定的!

也就是说widthMeasureSpec、heightMeasureSpec只是个建议,我们完全可以不接受并根据自己的实际情况来决定需要多大的尺寸

那么是谁调用了我们ChildView的onMeasure()方法,并把ChildView的布局参数传给它的呢?整个测量流程怎么实现的呢,我们再回到我们的ViewRootImpl类中看看

View树的测量流程:

View树的测量是从ViewRoolImpl对象中的mView(树根)开始的,我们来观察下mView是如何测量自己的大小的

ViewRootImpl.java

//ViewRootImpl.java
//开始线程,开始处理View树的 测量、布局、绘图
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //开始处理View树的 测量、布局、绘图
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

ViewRootImpl.java

//ViewRootImpl.java
private void performTraversals() {
    final View host = mView;
    ......
    if (layoutRequested) {

        final Resources res = mView.getContext().getResources();

        //根rootView的LayoutParams
        // public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
        WindowManager.LayoutParams lp = mWindowAttributes;

        //rootView期望的尺寸,为屏幕尺寸大小
        int desiredWindowWidth;
        int desiredWindowHeight;
        if (mFirst) {
            // make sure touch mode code executes by setting cached value
            // to opposite of the added touch mode.
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            ensureTouchModeLocally(mAddedTouchMode);
        } else {
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = true;

                if (shouldUseDisplaySize(lp)) {
                    // NOTE -- system code, won't try to do compat mode.
                    Point size = new Point();
                    //获取屏幕的大小
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                    //获取屏幕的大小
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }

        // Ask host how big it wants to be
        //开始测量View树
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
    }
    ......
    if (mApplyInsetsRequested) {
        mApplyInsetsRequested = false;
        mLastOverscanRequested = mAttachInfo.mOverscanRequested;
        dispatchApplyInsets(host);
        if (mLayoutRequested) {
            //开始测量View树
            windowSizeMayChange |= measureHierarchy(host, lp,
                    mView.getContext().getResources(),
                    desiredWindowWidth, desiredWindowHeight);
        }
    }
    ......
    //开始布局View树
    performLayout(lp, mWidth, mHeight);
    ......
    //开始Draw View树
    performDraw();
    ......

}

ViewRootImpl.java

//ViewRootImpl
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                 final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    //传给mView的模式和尺寸
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;

    //是否完成了mView的测量(标记是否完成测量)
    boolean goodMeasure = false;
    //lp.width=wrap_content(AT_MOST测量模式) (不知道mView需要多少尺寸) (三狗的流程,先拿预算经费,不够再说)
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        final DisplayMetrics packageMetrics = res.getDisplayMetrics();
        //获取Android配置的默认root View的尺寸大小(公司给大狗、二狗、三狗的团建经费预算)
        res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
        //初始建议大小(公司给大狗、二狗、三狗的团建经费预算)
        int baseSize = 0;
        if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
            baseSize = (int) mTmpValue.getDimension(packageMetrics);
        }
        //baseSize小于屏幕宽度(财务的经费预算baseSize不是公司的底线,公司经费底线是desiredWindowWidth)
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            //封装mView宽高模式和尺寸  MeasureSpec (方法在后面给出了)
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            //开始测量mView,(第一次测量)(方法在后面给出了)
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            //测量完成,获取mView的测量状态,MEASURED_STATE_TOO_SMALL(给的初始baseSize太小,不够mView使用)
            //如果mView(host)觉得baseSize够用
            if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
                //测量结束
                goodMeasure = true;
            } else { //给的baseSize不够用(此时三狗第一次打电话给财务妹子说经费不够用,需要加钱)
                //(desiredWindowWith+baseSize)/2 > baseSize (给三狗的经费增加了1000)
                baseSize = (baseSize + desiredWindowWidth) / 2;
                //用新的baseSize重新封装mView的宽度测量模式
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
               //开始测量mView,(第二次测量)(方法在后面给出了)
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                //如果mView(host)觉得新的baseSize够用
                if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
                    //测量结束
                    goodMeasure = true;
                }
            }
        }
    }

    //情况一:lp.width=wrap_content(上面还没有测量结束,baseSize增加了还不满足) (三狗第二次打电话给财务妹子说钱不够用了,财务妹子按照公司经费的底线desiredWindowWidth,又给三狗打了1000块钱)
    //情况二:lp.width=(精确的数值,EXATLY) (大狗的流程,明确提出500块的经费要求)
    //情况三:lp.width=match_parent(精确的数值,EXATLY) (二狗的流程,公司经费是多少,我就要多少)
    if (!goodMeasure) {
        //封装mView宽高模式和尺寸  MeasureSpec (方法在后面给出了)
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        //开始测量mView,(方法在后面给出了)
        //情况二、三 (大狗、二狗的情况) 是第一次测量
        //情况一 (三狗的情况) 是第三次测量
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }
    return windowSizeMayChange;
}

上面就是测量树根mView的流程,我们看看是怎么封装测量模式和尺寸的

ViewRootImpl.java

//ViewRootImpl.java
//生成mView的测量模式 MeasureSpec
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT://二狗的情况
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT://三狗的情况 windowSize(第一次是baseSize,第二次是(baseSize+desiredWindowWidth)/2,第三次是desiredWindowWidth)
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default://大狗的情况,明确知道自己的数值大小
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

开始测量,直接调用了mView(View.java)中的measure方法

//ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        //mView开始测量自己
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

在View.java中的measure()方法中,又调用了onMeasure()方法

View.java

//View.java
//widthMeasureSpec,heightMeasureSpec:ViewRootImpl中传给rootView的尺寸和模式(大狗,二狗,三狗的经费)
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    ......
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    //尺寸有变动(三狗狗的预算是否加了)
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    //是否是EXACTILY的情况(是否是大狗的情况,明确知道自己要500预算)
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    //是否是match_parent的情况(是否是二狗的情况,明确需要所有的经费预算)
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    //三狗的情况
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    if (forceLayout || needsLayout) {
        //是否缓存过测量尺寸
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        //缓存里没有或者忽略缓存
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //设置root View的尺寸
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {//缓存里有
            long value = mMeasureCache.valueAt(cacheIndex);
            //设置root View的尺寸
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
        }
    }
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
    //缓存测量的尺寸
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

通过onMeasure()方法设置mView的尺寸

View.java

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

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

在getDefaultSize()中从measureSpec中提取宽高尺寸大小

View.java

//View.java
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    //如果没有指定mView的测量模式,那么设置mView的宽高为建议的size大小
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

我们看看如果没有指定测量模式是如何获取建议的size

View.java

//View.java
protected int getSuggestedMinimumWidth() {
 //mView是否设置了背景图片?没有(设置为mMinWidth大小):有(取mMinWidth和图片尺寸最大值)
 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

现在大家明白是如何确定mView(根View)的尺寸了吗?

  • 这也是为什么,当我们在布局xml文件中,根布局写match_parent就是屏幕大小
  • 这也是为什么,在我们玩悬浮窗,popwindow等窗体布局时,我们在根布局明明写的wrap_content,窗体的大小总是match_parent

到这里大家可能还是会有疑问,mView(根View)自己的尺寸是确定了,如果它有子View,那它的子View是怎么测量呢,也就是我们的View树是怎么完成测量的。

如果mView有子View,那它就是个ViewGroup(实际上mView一定是ViewGroup,即使我们setContent(View),还是会被添加到ViewGroup中),既然它最终是调用onMeasure()来确定自己的尺寸,那只要ViewGroup重写onMeasure()方法,先遍历child并调用child.measure()方法完成child的尺寸设置,再调用setMeasuredDimension()设置自己的尺寸,不就用递归的方式完成了View树的测量吗?

可是我们在ViewGroup中,并没有发现onMeasure()方法,因为ViewGroup是个抽象类,并且ViewGroup不知道你的子View的摆放规则,无法算出ViewGroup自己的尺寸大小,所以,ViewGroup的onMeasure()方法肯定是在它的实现类中。

我们拿FrameLayout的onMeasure源码来看看,它是怎么实现child的测量和自身大小的确定的

FrameLayout.java

//FrameLayout.java
//widthMeasureSpec、heightMeasureSpec:
//当FrameLayout对象作为根View,widthMeasureSpec和heightMeasureSpec是ViewRootImpl中给的初始大小(baseSize,desiredWindowWidth)
//当FrameLayout对象作为子View,widthMeasureSpec和heightMeasureSpec是父ViewGroup的测量尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //获取子View的数量
    int count = getChildCount();
    //当前FrameLaoyut是否设置成了wrap_content(AT_MOST模式)
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                    MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    //一个View集合,存放子View的模式是match_parent的子View
    mMatchParentChildren.clear();
    int maxHeight = 0;
    int maxWidth = 0;
    //测量状态
    int childState = 0;
    //遍历Child
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        //当child设置成GONE时,会跳过测量
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //通知child测量自己  (方法后面给出)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            //获取child的布局参数
            final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
            //得出所有子View中宽高最大值
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            //组合测量状态
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            //如果当前FrameLayout是wrap_content模式,并且child是match_parent模式,则加入到mMatchParentChildren集合,在最后重新测量
            if (measureMatchParentChildren) {
                if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
                        lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

    //根据测量出来的child中的最大值和父View的尺寸,设置当前FrameLayout的大小,注意resolveSizeAndState()方法 (后面给出)
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    count = mMatchParentChildren.size();
    if (count > 1) {
        //遍历mMatchParentChildren (布局是match_parent的子View集合)
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();

            //重新测量
            final int childWidthMeasureSpec;
            if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                                lp.leftMargin + lp.rightMargin,
                        lp.width);
            }

            final int childHeightMeasureSpec;
            if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                                lp.topMargin + lp.bottomMargin,
                        lp.height);
            }
            //child测量
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

照着注释看,应该能看明白,这里有两个方法需要注意

  • measureChildWithMargins()
  • resolveSizeAndState()

measureChildWithMargins()

主要是看看child如何获取自己的布局参数并传递给自己的onMeasure()方法

ViewGroup.java

//ViewGroup.java
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed)
    //child的布局参数    
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //生成child的测量模式和尺寸(如何生成自己看看源码,不难)
    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将自己的布局模式传递给child.onMeasure()方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

resolveSizeAndState()

主要是看看这个state对测量有什么影响

View.java

//View.java
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    //FrameLayout的测量模式和尺寸
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;//封装的尺寸和状态
    switch (specMode) {
        case MeasureSpec.AT_MOST://如果FrameLayout的模式是wrap_content
            if (specSize < size) {//如果FrameLayout的尺寸小于child的尺寸
                //FrameLayout的尺寸太小啦,child的都比你大,放不下,child表示too_small
                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);
}

可以看到,当child尺寸比parent大,测量的时候会加上一个MEASURED_STATE_TOO_SMALL标记,是不是有点眼熟?

在ViewRootImpl根View的测量流程中有:

//ViewRootImpl
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                goodMeasure = true;
}

在前面的故事中,三狗多次反应给的团建经费不够,打电话反应要加钱

那么在我们的View测量过程中,如果不满意默认的根View尺寸大小,就可以通过设置MEASURED_STATE_TOO_SMALL这个标记,来让根View发起重新测量。

从这也可以看出,我们的onMeasure()方法可能被多次调用,不仅仅上面说道的三次,这也是很多初学者不明白为什么自己自定义的View,会被多次调用onMeasure、onLayout方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一鱼浅游

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值