最全的View绘制流程(下)— Measure、Layout、Draw

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

上文最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系说到了,我们打开一个Activity,经过WindowDecorView的创建过程后,绘制会从 ViewRootImpperformTraversals() 方法开始,从上到下遍历整个视图树进行View的绘制

ViewRootImp.performTraversals()

performTraversals()的源码非常的长,但是核心代码就是下面三个步骤。

private void performTraversals() {
    ......
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ......
    //这里的mView就是DecorView
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    mView.draw(canvas);
    ......
 }

所以,一个完整的绘制流程包括measurelayoutdraw三个步骤,其中:

  • measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来
  • layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置
  • draw:绘制。确定好位置后,就将这些控件绘制到屏幕上

每个 View 负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作

Measure — 测量

MeasureSpec

从上面的源码我们可以发现,系统会用一个int型的值(childWidthMeasureSpecchildHeightMeasureSpec)来存储View的宽高的信息

上文中用于生成childWidthMeasureSpecchildHeightMeasureSpecgetRootMeasureSpec()方法

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

这个方法实际上调用的就是MeasureSpec.makeMeasureSpec()

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    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;

   
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return makeMeasureSpec(size, UNSPECIFIED);
        }
        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);
    }
}

那么这个MeasureSpec到底是什么意思呢?MeasureSpec概括了从父布局传递给子view的布局要求,包括了测量模式和测量大小。分析代码可知,int长度为32位,高2位表示mode(模式),后30位用于表示size(大小)

有三种mode

UNSPECIFIED:不对View大小做限制,如:ListView,ScrollView
EXACTLY:确切的大小,如:100dp或者march_parent
AT_MOST:大小不可超过某数值,如:wrap_content

子View的LayoutParams / 父view的MeasureSpec EXACTLY AT_MOST UNSPECIFIED
具体大小(如100dp) EXACTLY EXACTLY EXACTLY
match_parent EXACTLY AT_MOST UNSPECIFIED
wrap_content AT_MOST AT_MOST UNSPECIFIED

View采用固定宽高时(即设置固定的dp/px),不管父容器是什么模式,View都是EXACTLY模式,并且大小遵循我们设置的值

View的宽高是match_parent时,如果父容器的是EXACTLY模式,那么View也是EXACTLY模式且其大小是父容器的剩余空间;如果父容器是AT_MOST模式那么View也是AT_MOST模式并且其大小不会超过父容器的剩余空间

View的宽高是wrap_content时,View都是AT_MOST模式并且其大小不能超过父容器的剩余空间

只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以确定出子元素的MeasureSpec,进一步便可以确定出测量后的大小

onMeasure

mView.measure()内部会调用onMeasure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //仅列出关键代码
    ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

一个View的实际测量工作是在onMeasure()中实现的,onMeasure()已经默认为我们的控件测量了宽高

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
 * 为宽度获取一个建议最小值
 */
protected int getSuggestedMinimumWidth () {
    return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
/**
 * 获取默认的宽高值
 */
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;
}

在自定义ViewGroup时,默认的onMeasure()往往不能满足我们的需求,这时候就要重写该方法,在该方法内测量子View的尺寸。当重写onMeasure()时,必须调用setMeasuredDimension(width,height)来存储该View测量出的宽和高。如果不这样做将会触发IllegalStateException

ViewGroup提供了三个方法测量子View的宽高

/**
  *遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
  */
protected void measureChildren (int widthMeasureSpec, int heightMeasureSpec) {
...
}
 
/**
* 测量某一个child的宽高
*/
protected void measureChild (View child, int parentWidthMeasureSpec,
...
}
 
/**
* 测量某一个child的宽高,考虑margin值
*/
protected void measureChildWithMargins (View child,
...
}

View和ViewGroup重写onMeasure的差异

下面用两个例子分别来展示一下ViewViewGroup重写onMeasure的差异

View

View一般只关心自身尺寸的测量

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        Log.e("CustomTextViewWidth", "---speSize = " + specSize + "");

        switch (specMode) {
            case MeasureSpec.AT_MOST:
                result = (int) mPaint.measureText(mTextStr) + getPaddingLeft() + getPaddingRight();

                Log.e("CustomTextViewWidth", "---speMode = AT_MOST");
                break;
            case MeasureSpec.EXACTLY:
                Log.e("CustomTextViewWidth", "---speMode = EXACTLY");
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
                Log.e("CustomTextViewWidth", "---speMode = UNSPECIFIED");
                result = Math.max(result, specSize);
        }
        Log.e("CustomTextViewWidth", "---result = "+result);
        return result;
    }


    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        Log.e("CustomTextViewHeight", "---speSize = " + specSize + "");

        switch (specMode) {
            case MeasureSpec.AT_MOST:
                result =
                        (int) (-mPaint.ascent() + mPaint.descent()) + getPaddingTop() + getPaddingBottom();
                Log.e("CustomTextViewHeight", "---speMode = AT_MOST");
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                Log.e("CustomTextViewHeight", "---speSize = EXACTLY");
                break;
            case MeasureSpec.UNSPECIFIED:
                result = Math.max(result, specSize);
                Log.e("CustomTextViewHeight", "---speSize = UNSPECIFIED");
                break;
        }
        Log.e("CustomTextViewHeight", "---result = "+result);
        return result;
    }

ViewGroup

ViewGroup一般会先遍历子View,调用子View的测量方法,然后在再结合子View的尺寸来确定自身的大小

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        // 如果是wrap_content,定义width,height统计FlowLayout的宽高
        int width = 0;
        int height = 0;
        // 记录每一行的宽度与高度
        int lineWidth = 0;
        int lineHeight = 0;
        /**
         * 1、通过getChildCount,获取子View的个数view个数
         */
        int childCount = getChildCount();
        /**
         * 2、遍历childCount,通过getChildAt获取到对应的view
         */
        for (int i = 0; i < childCount; i++) {
            //获取i对应的子View,通过获取他的宽高,确定
            View childView = getChildAt(i);
            /**
             * 3、对childView进行测量
             */
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            // 判断是否换行,如果换行则高度累加,如果不换行则宽度累加
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
                // 对比得到最大的宽度
                width = Math.max(width, lineWidth);
                // 重置lineWidth
                lineWidth = childWidth;
                // 记录行高
                height += lineHeight;
                // 重置lineHeight
                lineHeight = childHeight;
            } else {
                // 宽度累加
                lineWidth += childWidth;
                // 得到当前行最大的高度
                lineHeight = Math.max(lineHeight, childHeight);
            }
            // 最后一个的时候,不管是换行,还是未换行,前面都没有处理
            if (i == childCount - 1) {
                width = Math.max(width, lineWidth);
                height += lineHeight;
            }
            /**
             * 4、确定父布局(FlowLayout)的宽高
             */
            setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth :
                            width + getPaddingLeft() + getPaddingRight(),
                    modeHeight == MeasureSpec.EXACTLY ? sizeHeight :
                            height + getPaddingTop() + getPaddingBottom());
        }
    }

Layout - 布局

前面measure的作用是测量每个View的尺寸,而layout的作用是根据前面测量的尺寸以及设置的其它属性值,共同来确定View的位置

ViewGroup的layout()

@Override
public final void layout(int l, int t, int r, int b) {
      if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
          if (mTransition != null) {
              mTransition.layoutChange(this);
          }
          //内部实际上是调用View的layout()
          super.layout(l, t, r, b);
      } else {
          // record the fact that we noop'd it; request layout when transition finishes
          mLayoutCalledWhileSuppressed = true;
      }
}

从源码中可以看出实际上调用的还是View的layout()方法

View的layout()

/**
 * 作用:确定View本身的位置,即设置View本身的四个顶点位置
 */
public void layout(int l, int t, int r, int b) {

    //根据一些flag,如果有需要则进一步measure
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 & = ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    //暂存旧的位置信息
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 1. 确定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化
    boolean changed = isLayoutModeOptical (mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若视图的大小 & 位置发生变化
    // 会重新确定该View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
        // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
        onLayout(changed, l, t, r, b);

        //回调layoutChange事件
        ......
    }
    //标记为已经执行过layout;
    ......
}

/**
 * 作用:确定View本身的位置,即设置View本身的四个顶点位置
 * @return 如果新的尺寸和位置和之前的不同,返回true
 */
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ......
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        ......
        int oldWidth = mRight -mLeft;
        int oldHeight = mBottom -mTop;
        int newWidth = right -left;
        int newHeight = bottom -top;
        boolean sizeChanged =(newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);

        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        ......
    }
    return changed;
}

从源码可以看出layout()最终通过setFrame()方法对view的四个属性(mLeft、mTop、mRight、mBottom)进行了赋值,从而去确定View的大小和位置,如果发生改变则调用onLayout()方法

onLayout

我们先来看看ViewGrouponLayout()方法,该方法是一个抽象方法。因为layout过程是父布局容器布局子View的过程,onLayout()方法对子View没有意义,只有ViewGroup才有用,所以ViewGroup应该重写该方法并为每一个子View调用layout()

protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

我们再来看看顶层ViewGroup,也就是DecorViewonLayout()方法。DecerView继承自FrameLayout,所以我们直接看FrameLayoutonLayout()方法

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
        ......
        for (int i = 0; i < count; i++) {
             final View child = getChildAt(i);
             if (child.getVisibility() != GONE) {
                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                 final int width = child.getMeasuredWidth();
                 final int height = child.getMeasuredHeight();
                 ......
                 child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
    }

我们可以看到,这里面会对每一个child调用layout()方法。如果该child仍然是ViewGroup,会继续递归下去;如果是叶子View,则会走到ViewonLayout空方法,该叶子View布局流程就走完了。另外,widthheight分别来源于measure阶段存储的测量值

Draw - 绘制

layout完成后,就进入到draw阶段了,在这个阶段,会根据layout中确定的各个view的位置将它们画出来

前面说过,mView就是DecorView,所以我们直接来看DecorViewdraw()方法

@Override
public void draw(Canvas canvas) {
     super.draw(canvas);

     if (mMenuBackground != null) {
         mMenuBackground.draw(canvas);
     }
}

调用完super.draw()后,还画了菜单背景。我们继续关注super.draw()方法,会发现FrameLayout和ViewGroup都没有重写该方法,直接进到了Viewdraw()方法


@CallSuper
public void draw(Canvas canvas) {
    ......

  
    int saveCount;

    // Step 1, draw the background, if needed
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // Step 2, If necessary, save the canvas' layers to prepare for fading
    ......
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);
  
    //Step 5, If necessary, draw the fading edges and restore layers
    ......
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);45         ......
}

主要是就是如下几步,其中最重要的就是画内容画子View

  • 画背景。对应我我们在xml布局文件中设置的android:background属性

  • 画内容。通过重写onDraw()方法

  • 画子ViewdispatchDraw()方法用于帮助ViewGroup来递归画它的子View

  • 画装饰。这里指画滚动条和前景。其实平时的每一个View都有滚动条,只是没有显示而已

onDraw()

当自定义View需要进行绘制的时候,我们往往会重写onDraw()方法,这里放一个简单的例子感受一下

Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 

@Override  
protected void onDraw(Canvas canvas) {  
     mPaint.setColor(Color.YELLOW);  
     canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);  
     mPaint.setColor(Color.BLUE);  
     mPaint.setTextSize(20); 
     String text = "Hello View";  
     canvas.drawText(text, 0, getHeight() / 2, mPaint);  
}  

总结

本文主要讲解View的绘制流程,让读者们从大的方向上对View的绘制有一个了解。具体自定义View如何进行measurelayoutdraw这些细节,以后有时间会专门出一个自定义View系列进行讲解

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