无标题文章


View的测量

前言

        我们做Android开发的目的就是把产品设计的东西运行到我们的手机上,说白了就是把设计图搬到我们的手机上,其中跟我们打交道最多的就是View, 所以我们有必要了解它是个什么东西,又是怎么把我们需要的东西展现在手机屏幕上,这就涉及到了View的测量、布局和绘制。这里我将分三个章节来一一向你展示,本节我们先来分析View的测量。在分析源码之前,我们先问自己几个问题,什么是测量?测量是干什么的?那些东西需要测量?从哪里开始测量?带着这些问题我们开始进行今天的答疑之旅

1. 测量是什么?它是干什么的?

        测量就是描述一个物体所占据的空间,也就是它的长宽高,是一个物体本身的属性

2. 为什么需要测量

    回答这个问题之前我们要明白我们要干什么,我们是要把我们的xml文件里面的view绘制到手机屏幕上,既然我们要把view绘制到屏幕上那我们总得知道我们绘制的view有多大吧,这就是我们为什么需要测量

3. 那些东西需要测量?

    我们绘制到屏幕上的view要么是单个View,要么是一组View即ViewGroup,因此需要测量的也就是View和ViewGroup

4. 从哪里开始测量?

    解答了上面的问题接下来我将为大家讲解从哪里开始测量以及单个View和ViewGroup(一组View)怎么测量

从哪里开始测量

     刚我也说了我们测量是要把xml文件中的view,添加绘制到手机屏幕上,所以测量的开始从添加view开始,通过Activity的启动流程到View的显示我们知道,view的添加是在ViewRootImpl中进行的,调用的是addView() -> setView() -> requestLayout(),不明白的可以看我之前的文章,接下来我们从requestLayout()开始分析

public void requestLayout() {

    if (!mHandlingLayoutInLayoutRequest) {

        checkThread();

        mLayoutRequested = true; //大家在这里先记住这个标记,在后面很重要

        scheduleTraversals();

    }

}

void scheduleTraversals() {

.... 省略部分代码

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

}

final class TraversalRunnable implements Runnable {

        @Override

        public void run() {

            doTraversal();

        }

    }

void doTraversal() {

    performTraversals();     

}

通过查看源码发现其内部调用的是performTraversals(), 这又是何方神圣呢?我们进入代码在仔细看个清楚

private void performTraversals() {

//注意 mLayoutRequested直接决定了layoutRequested,也在很大程度上决定了performMeasure()

boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);

if (layoutRequested) {

      windowSizeMayChange |= measureHierarchy(host, lp, res,

                    desiredWindowWidth, desiredWindowHeight);   

    }


    boolean windowShouldResize = layoutRequested && windowSizeMayChange

            && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())

                || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&

                        frame.width() < desiredWindowWidth && frame.width() != mWidth)

                || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&

                        frame.height() < desiredWindowHeight && frame.height() != mHeight));


    // 满足任意添加就会进行测量

    if (mFirst || windowShouldResize || insetsChanged ||

                viewVisibilityChanged || params != null || mForceNextWindowRelayout) { 

        //获取顶层布局的宽高测量规格,执行测量方法

        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);

        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    }

    ....


    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);

    //测量走了,layout方法一般也会走

    if (didLayout) {

        performLayout(lp, mWidth, mHeight);

    }

    ....


    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {

    //在这里进行页面的绘制

    performDraw();

    }

}

原来我们的测量、布局和绘制都是在这个方法里面完成的,今天我们分析的是测量,布局和绘制等到下个章节在讲,接下来我们就看看performMeasure()

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

        if (mView == null) {

            return;

        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");

        try {

          //注意这个mView其实就是decorView, 本质上是一个ViewGroup

            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        } finally {

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);

        }

    }

接下来会走到ViewGroup的measure(), 通过源码发现ViewGroup没有measure(), 所以我们只有查看它的父类View, 我们来看看View的measure()做了什么

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

        boolean optical = isLayoutModeOptical(this);

        if (optical != isLayoutModeOptical(mParent)) {

            Insets insets = getOpticalInsets();

            int oWidth  = insets.left + insets.right;

            int oHeight = insets.top  + insets.bottom;

            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);

            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);

        }

      onMeasure(widthMeasureSpec, heightMeasureSpec);

    } 

View的measure方法判断了一下View的模式,就直接调用onMeasure(), 是不是觉得很熟悉,没错我们自定义view的时候就需要复写该方法,之前我们也说过测量有View的测量和ViewGroup的测量,我们先来看看View的测量即onMeasure()

View的测量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

View的onMeasure()中得到一个默认值就直接设置给了view,在获取默认值时调用了getSuggestedMinimumWidth(),我们来看看这个方法里面做了什么

protected int getSuggestedMinimumWidth() {

    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

根据方法名我们知道得到的是一个建议的最小值,首先我们要明白mBackground是谁,mBackground是我们给view设置的背景,为什么要在这里判断mBackground是否为null呢?这是因为如果我们给view设置了背景,为了凸显出该view设置了背景,系统就会给背景设置一个最小宽度。 mMinWidth这是从哪里来的呢?别急这个值是你自己设置的,我们写的xml文件在给手机屏幕做适配的时候会用到,如:android:minWidth="20dp",如果不设置的话默认为0。我们在看看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;

    }

注意这里AT_MOST和EXACTLY模式下得到的值是一样的,接下来我们再来看看setMeasuredDimension()设置测量的view的宽高

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {

        boolean optical = isLayoutModeOptical(this);

        //判断父view是否有视觉模式,如果有在加上视觉模式所需要的宽高

        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) { //最终在这里对view的宽高进行保留

        mMeasuredWidth = measuredWidth;

        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

    }

在这里会保存测量的宽高,后面我们使用的getMeasureWidth()、getMeasureHight()得到的值就是在这里保留的值。至此view的测量到此结束,我们来看看ViewGroup的测量吧,ViewGroup是一个抽象类,onMeasure()也是抽象的,我们看看他的子类,我们知道Android有四大布局,即FrameLayout、RelativeLayout、LinearLayout及AbsoultLayout, AbsoultLayout绝对布局,这个我们很少用到,FrameLayout是最简单的布局,我们就先看看它内部是怎么实现的。

ViewGroup的测量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//1、获取所有的子类

        int count = getChildCount();

//2、判断子类里面的宽高是否有设置match_parent

        final boolean measureMatchParentChildren =

                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||

                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

        //将存放width或height为match_parent的view集合清空     

        mMatchParentChildren.clear();

//3、定义变量存储获取的子类的最大宽高

        int maxHeight = 0;

        int maxWidth = 0;

        int childState = 0;

//4、遍历所有的子类,获得其宽高,取出子类中最大的宽和高

        for (int i = 0; i < count; i++) {

            final View child = getChildAt(i);

            if (mMeasureAllChildren || child.getVisibility() != GONE) {

                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

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

                //5、如果子类的宽高有设置match_parent,则将其添加到数组中

                if (measureMatchParentChildren) {

                    if (lp.width == LayoutParams.MATCH_PARENT ||

                            lp.height == LayoutParams.MATCH_PARENT) {

                        mMatchParentChildren.add(child);

                    }

                }

            }

        }

        //6、将子类获得的最大值加上父类设置的padding值,得到一个最大值

        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();

        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // 7、再次检验,将获得的最大值和建议的最小值进行比较取得最终的最值

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

        }

// 8、设置frameLayout的宽高,frameLayout的宽高是通过测量子类的宽高,得其最大的宽高在加上padding,在进过一系列的判断才最终得到的

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),

                resolveSizeAndState(maxHeight, heightMeasureSpec,

                        childState << MEASURED_HEIGHT_STATE_SHIFT));

// 9、获取子类中宽或高设置match_parent的个数,如果其个数大于0,我们就需要对这些view重新测量,

        count = mMatchParentChildren.size();

        if (count > 1) {

            for (int i = 0; i < count; i++) {

                final View child = mMatchParentChildren.get(i);

                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;

                if (lp.width == 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 == 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.measure(childWidthMeasureSpec, childHeightMeasureSpec);

            }

        }

    }

看完FrameLayout的测量不知道你们有没有理解清楚,心里是否还有疑问?有人说你说的已经很清楚了没有什么问题的,哪好我在问你们一个问题,子View为什么需要进行二次测量,是这样的当view的宽或者高设置match_parent的时候,子类自己也不知道自己的宽高到底是多少,只知道和父类一样就行了,当父类的宽高确定了之后,就需要对那些设置了match_parent的view重新测量,设置宽高。

总结

       源码分析完了接下来我们来回顾总结一下,测量是从ViewRootImpl中的requestLayout()方法发起,经过一系列的操作最终调到performTraversals(), 在该方法中会依次调用performMeasure()、performLayout()、performDraw()。 在requestLayout()中有一个参数mLayoutRequested很重要,它很大程度上决定了performMeasure()和performLayout()是否调用,对performDraw()没有什么影响,我们先来看performMeasure(), 发现其内部最终调用的是View的measure(),最终调用到我们最熟悉的onMeasure(). 这时我们就分别介绍了View的onMeasure()和ViewGroup的onMeasure()

1. View的onMeasure()

* View的onMeasure()很简单,直接调用setMeasuredDimension(), 设置View的宽高,通过setMeasuredDimensionRaw()保留我们设置的宽高,使用的时候通过getMeasureWidth()、getMeasureHeight()来获取我们设置的宽高

2. ViewGroup的onMeasure()

* ViewGroup的onMeasure()很复杂,ViewGroup的宽高是通过子类的宽高来决定的,所以需要先测量所有子类的宽高,得到子类宽高的最值,然后通过子类的宽高在加上一些padding值及其他条件的校验最终得出ViewGroup的宽高,在设置ViewGroup的宽高,然后再重新测量子类宽高设置为match_parent的view

接下来我们看一张就一目了然了

![1.png](/Users/apple/Desktop/blog/View的测量.png)

requestLayout()、invalidate()、 postInvalidate()的区别

  其实这个跟本章内容不是很搭,为什么在这里提出来呢,因为他们之间的区别在本文的代码里面就能够很好的区别开来,源码不是很多我就不一一展示了,想弄清楚的同学可以自己翻源码好好看看。

* requestLayout(): 我们就不用多说了,本文的起点就是从它开始的,通过它才有了后面View的measure、layout和draw。

* invalidate(): 这个方法我们也不陌生,用来更新界面,这时我们就要问了它更新界面的时候还会走measure和layout吗?我想这个你自己也不清楚吧,进入源码我们可以看到View的invalidate()会递归的调用 parent.invalidateChildInParent(),最终调用ViewRootImpl的invalidateChildInParent(), 通过查看源码得知ViewRootImpl的invalidateChildInParent()内部调用的是invalidate(), 注意这个invalidate()是ViewRootImpl中的,而invalidate()内部调用的是scheduleTraversals(),看到这里我们就熟悉了下面的操作了,看到这里的同学就知道了接下来就要执行performTraversals(),在接着执行performMeasure()、performLayout()和performDraw()了,慢着,这里需要暂停一下,还记得我在前面一直说的要你注意mLayoutRequested这个标记吗?当我们requestLayout()之后会将其置为false,只有在调用requestLayout的时候将其置为true。当你调用invalidate()的时候该标记依然为false,就不会走performMeasure()和performLayout(),直接走的是performDraw()。

* postInvalidate(): 其内部还是调用的是invalidate(), 只不过它可以在子线程通知更新UIblo

实战应用

       至此我们对View的测量流程已经有了大致的了解了,可能有人就要问了,哪这些对我们实际开发中有什么用呢?实际开发的时候有用到吗?同学别急,这个大有用处,听我为你娓娓道来。在项目中根据需求我们需要自定义view,这里我们给自定义的View分为两种,一种是根据系统已有的view,然后根据需要自定义我们所需要的,另一种是完全的自定义。

1. 根据系统已有View自定义我们需要的View

举个例子:我们需要一个方形的ImageView,无论你宽高怎么设置我得到的都是方形的。看到这个需求我们首先想到的是系统给我们提供了Imageview,但系统的满足不了我们的需求,ImageView的宽高究竟是怎么测量的我们不得而知,但是我们可以获取系统帮我们测量的imageview的宽高,然后我们在将它的宽高设置成一样的就行了

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int measuredHeight = getMeasuredHeight();

        int measuredWidth = getMeasuredWidth();

        int result = 0;

        if (measuredHeight > measuredWidth) {

            result = measuredHeight;

        } else {

            result = measuredWidth;

        }

        setMeasuredDimension(result, result);

    }

2. 系统没有定义的,完全由我们自定义

由于是完全的自定义,系统也没法帮我们测量,需要我们自己计算自己所需要的宽高,所以不需要super.onMeasure(), 在父类所限制的测量模式下计算出我们所需要的宽高,最后调用setMeasuredDimension()设置我们的宽高。另一种方式是直接计算我们需要的宽高,计算完了之后调用resolveSize(size, measureSpec)校验我们计算的宽高,其中measureSpec是父类对子类的宽高测量规格。然后在调用setMeasuredDimension()设置宽高。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容