这次来说一说view的绘制过程,同样也算是个笔记的东西,梳理一下大致的过程,忽略了内部很多的条件判断等。上次说了setcontentview和infalter的一些简单过程,这个是用来把view从XML文件的格式解析和加载,但是具体view的怎么绘制到界面上的并没有提及,还有上次留下的问题,有时间学习了activity的启动流程再来说一下,现在开始吧。
  网上有很多的文章说view的绘制,无非就是三个方法onMeasure,onLayout,onDraw,但是这仅仅是冰山一角,这三个方法的调用时机和内部的大致运行逻辑并没有说清楚,现在先从view开始的地方说起。
  view从什么地方开始绘制呢,先说结论吧,因为没有一些铺垫即使从开始的地方说起也是云里雾里,说完整个的大致过程,再去看开始的地方也就顺其自然了。view的绘制是从ViewRootImpl 的performTraversals方法开始的,翻译就是递归调用,我们已经知道,view是嵌套存在的,所以要去绘制整个的view肯定要去嵌套调用一些方法,(ps:这个源码是在API22下看的,在23的时候又有一些改变,但是整个的逻辑是没有变化的)。我们找到这个方法,这是个非常长的方法,一共有782行,所以会去忽略很多的判断条件,只所我们关注的三个重点方法,首先找到这里:
Measure
| 1 2 3 4
 | int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 | 
这里就是进行view测绘的地方,先去getRootMeasureSpec方法里面看看,
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 | 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:                          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);             break;         default:                          measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);             break;         }         return measureSpec;     }
 | 
这是个组装参数的地方,像我们平常说的MATCH_PARENT对应EXACTLY,WRAP_CONTENT对应AT_MOST这里就看的很清楚了,而且这个根视图,之前提到过的,肯定是走MATCH_PARENT,这就是我们根视图总是全屏的原因。
  回到前面还有个performMeasure方法,看注释也知道是干啥的,进去看看:
| 1 2 3 4 5 6 7 8
 | private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {         Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");         try {             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);         } finally {             Trace.traceEnd(Trace.TRACE_TAG_VIEW);         }     }
 | 
看,来到了view的measure方法,也就是说measure方法的两个参数其实都是父view的,下面还可以看到,真正决定一个view的大小是这父view的两个参数和子view共同决定的。下面就进到了view里面。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
 |     * <p>     * This is called to find out how big a view should be. The parent     * supplies constraint information in the width and height parameters.     * </p>     *     * <p>     * The actual measurement work of a view is performed in     * {@link #onMeasure(int, int)}, called by this method. Therefore, only     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.     * </p>     *     *     * @param widthMeasureSpec Horizontal space requirements as imposed by the     *        parent     * @param heightMeasureSpec Vertical space requirements as imposed by the     *        parent     *     * @see #onMeasure(int, int)     */    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {    ...各种不明白的东西。。😂           onMeasure(widthMeasureSpec, heightMeasureSpec);            ...继续各种不明白的东西。。😂    }
 | 
这里主要还是看注释,四级水平理解一下,这个方法调用用来测量view应该有多大,父布局在宽高里面提供限制信息,也就是前面说的view的大小由爹和儿子一起决定,还有就是实际的测量工作是在子类的onMeasure方法里面搞,因为measure是final的。这就是平时自定义view的时候要去从写这个方法的原因,别急,还没完,进去看看。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
 |      * <p>      * Measure the view and its content to determine the measured width and the      * measured height. This method is invoked by {@link #measure(int, int)} and      * should be overridden by subclasses to provide accurate and efficient      * measurement of their contents.      * </p>      *      * <p>      * <strong>CONTRACT:</strong> When overriding this method, you      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the      * measured width and height of this view. Failure to do so will trigger an      * <code>IllegalStateException</code>, thrown by      * {@link #measure(int, int)}. Calling the superclass'      * {@link #onMeasure(int, int)} is a valid use.      * </p>      *      * <p>      * The base class implementation of measure defaults to the background size,      * unless a larger size is allowed by the MeasureSpec. Subclasses should      * override {@link #onMeasure(int, int)} to provide better measurements of      * their content.      * </p>      *      * <p>      * If this method is overridden, it is the subclass's responsibility to make      * sure the measured height and width are at least the view's minimum height      * and width ({@link #getSuggestedMinimumHeight()} and      * {@link #getSuggestedMinimumWidth()}).      * </p>      *      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.      *                         The requirements are encoded with      *                         {@link android.view.View.MeasureSpec}.      * @param heightMeasureSpec vertical space requirements as imposed by the parent.      *                         The requirements are encoded with      *                         {@link android.view.View.MeasureSpec}.      *      * @see #getMeasuredWidth()      * @see #getMeasuredHeight()      * @see #setMeasuredDimension(int, int)      * @see #getSuggestedMinimumHeight()      * @see #getSuggestedMinimumWidth()      * @see android.view.View.MeasureSpec#getMode(int)      * @see android.view.View.MeasureSpec#getSize(int)      */     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));     }
 | 
可以去读一读注释,简单说一下,这个方法是用来测量view和它的内容,而且一定要调用setMeasuredDimension把测量的结果穿进去,基类默认实现的背景的大小(这个在下面看到)。如果重写了这个方法,你得搞清楚这个view的大小至少比最小的大(后面会说)。好吧,蹩脚的英文。也就是说当我们测量完view之后,再setMeasuredDimension,我们的工作就结束了。那么这个方法是什么东西,去看看:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
 | 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;     }
 | 
可以看到,默认的时候,当设置wrap_content和march_parent都是默认的父布局的大小,这里的size是进一步getSuggestedMinimumWidth传进来的,而这个的大小是
| 1 2 3
 | protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }
 | 
看有没有背景,这个默认是0,也可以在XML中设置,这样也说完了前面的两个问题。
  到这里,似乎view的测量就完了,但是不要忘记,view是嵌套的,要去测量完整个的view,还得去viewgroup里面看看,viewgroup里面有三个方法:measureChildren, measureChild, measureChildWithMargins,一个个看。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 |      * Ask all of the children of this view to measure themselves, taking into      * account both the MeasureSpec requirements for this view and its padding.      * We skip children that are in the GONE state The heavy lifting is done in      * getChildMeasureSpec.      *      * @param widthMeasureSpec The width requirements for this view      * @param heightMeasureSpec The height requirements for this view      */     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];             if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {                 measureChild(child, widthMeasureSpec, heightMeasureSpec);             }         }     }
 | 
遍历所以的子view,去测量,如果是gone的,就跳过。然后进入measureChild,提醒一下,这里传进去的widthMeasureSpec和heightMeasureSpec都是父view的,用来在子view测量的时候共同决定大小用的。这里的measureChild只包含Padding,measureChildWithMargins还包含Margin,调复杂的看:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
 | 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);     }
 | 
看注释,然后看重要的getChildMeasureSpec:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
 | public static int getChildMeasureSpec(int spec, int padding, int childDimension) {         int specMode = MeasureSpec.getMode(spec);         int specSize = MeasureSpec.getSize(spec);         int size = Math.max(0, specSize - padding);         int resultSize = 0;         int resultMode = 0;                  switch (specMode) {                           case MeasureSpec.EXACTLY:             if (childDimension >= 0) {                                 resultSize = childDimension;                                  resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.MATCH_PARENT) {                                                   resultSize = size;                                   resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.WRAP_CONTENT) {                                                                                     resultSize = size;                                  resultMode = MeasureSpec.AT_MOST;             }             break;                             }         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);     }
 | 
注释已经很详细了,这就是我们见到的父view和子view的在某些关系下面的布局样式。这次明白了吧。整个逻辑大概说一下,rootview获得宽高和模式,通过MeasureSpec传给下一级,一般是viewgroup,然后viewgroup主要调用getChildMeasureSpec根据上面的MeasureSpec子view的childDimension来确定自己viewgroup的大小,如果还有子view,把计算出来的自己的MeasureSpec再传下去.在viewgroup中我们并没有看到调用setMeasuredDimension方法去把测量的值传递进去,这个是因为viewgroup是个抽象类,具体的测量方式需要我们自己去实现,比如可以去看看LinearLayout的实现,就在测量完成后调用了setMeasuredDimension方法。所以在一般自定义viewgroup的时候,可以先去调用measureChildren去测量完全部的子view,这样就可以去获取子view的宽高,然后再干什么就随你了,最后不要忘记去调用setMeasuredDimension方法。
layout
  现在整个view的测绘就差不多结束了,下一步就是开始layout的步骤。先去看viewgroup里面的:
| 1 2 3 4 5 6 7 8 9 10 11 12
 | @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);            }            super.layout(l, t, r, b);        } else {                        mLayoutCalledWhileSuppressed = true;        }    }
 | 
其实还是调用了view里面的layout方法,进去看看:
| 1 2 3 4 5 6 7 8 9
 | public void layout(int l, int t, int r, int b) {         ......         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {                          onLayout(changed, l, t, r, b);             ......         }         ......     }
 | 
和measure的过程差不多,还是去调用onLayout方法,看一下
| 1 2
 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {     }
 | 
空的,那viewgroup的呢:
| 1 2 3
 | @Override     protected abstract void onLayout(boolean changed,             int l, int t, int r, int b);
 | 
也是空的,还变成抽象的了,那总结一下:view的layuout方法并没有像measure方法一下是final的,但是也是不推荐去重写的,如果有需要还是去重写onLayout方法,而在viewgroup中,layout变成了final,onLayout方法变成抽象的了,这就要求子类必须去实现这个过程,也就是要去子类自己去决定里面的view怎么去排布,所以去找个子类看看,还是拿LinearLayout看:
| 1 2 3 4 5 6 7 8
 | @Override     protected void onLayout(boolean changed, int l, int t, int r, int b) {         if (mOrientation == VERTICAL) {             layoutVertical(l, t, r, b);         } else {             layoutHorizontal(l, t, r, b);         }     }
 | 
看名字就知道了,不解释,选个layoutVertical看看:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
 | void layoutVertical(int left, int top, int right, int bottom) {         final int paddingLeft = mPaddingLeft;         int childTop;         int childLeft;                           final int width = right - left;                  int childRight = width - mPaddingRight;                                    int childSpace = width - paddingLeft - mPaddingRight;                  final int count = getVirtualChildCount();                  final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;         final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;         switch (majorGravity) {            case Gravity.BOTTOM:                                childTop = mPaddingTop + bottom - top - mTotalLength;                break;                            case Gravity.CENTER_VERTICAL:                childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;                break;            case Gravity.TOP:            default:                childTop = mPaddingTop;                break;         }         for (int i = 0; i < count; i++) {             final View child = getVirtualChildAt(i);             if (child == null) {                 childTop += measureNullChild(i);                              } else if (child.getVisibility() != GONE) {                 final int childWidth = child.getMeasuredWidth();                 final int childHeight = child.getMeasuredHeight();                                  final LinearLayout.LayoutParams lp =                         (LinearLayout.LayoutParams) child.getLayoutParams();                                  int gravity = lp.gravity;                 if (gravity < 0) {                     gravity = minorGravity;                 }                 final int layoutDirection = getLayoutDirection();                 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);                 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {                     case Gravity.CENTER_HORIZONTAL:                         childLeft = paddingLeft + ((childSpace - childWidth) / 2)                                 + lp.leftMargin - lp.rightMargin;                         break;                     case Gravity.RIGHT:                         childLeft = childRight - childWidth - lp.rightMargin;                         break;                     case Gravity.LEFT:                     default:                         childLeft = paddingLeft + lp.leftMargin;                         break;                 }                 if (hasDividerBeforeChildAt(i)) {                     childTop += mDividerHeight;                 }                 childTop += lp.topMargin;                                  setChildFrame(child, childLeft, childTop + getLocationOffset(child),                         childWidth, childHeight);                 childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);                 i += getChildrenSkipCount(child, i);             }         }     }
 | 
整个的layout过程比measure好理解,还要提一下,平时使用的时候会遇到getMeasuredWidth和getWidth,这里就差不多可以理解了,第一个是在measure之后可以获取到,而第二个需要在layout之后才可以获取到,一般都是一样的。总结一下:
measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的,看前面的那篇。到这里,layout的过程也差不多了,该到了绘制的时候了。
draw
view的绘制过程是比较复杂的,贴一张图,来自工匠若水

因为viewgroup没有去重写draw方法,直接去看view里面的:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
 | public void draw(Canvas canvas) {        ...                 * Draw traversal performs several drawing steps which must be executed         * in the appropriate order:         *         *      1. Draw the background         *      2. If necessary, save the canvas' layers to prepare for fading         *      3. Draw view's content         *      4. Draw children         *      5. If necessary, draw the fading edges and restore layers         *      6. Draw decorations (scrollbars for instance)         */       ...
 | 
注释写的很清楚了,一共分为6步,其中2,5可以省略,一步步来吧
第一步,对View的背景进行绘制。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 | private void drawBackground(Canvas canvas) { //平时设置的setbackground,实质都转变为一个Drawable         final Drawable background = mBackground;         ...         //设置绘制范围         setBackgroundBounds();         ...         final int scrollX = mScrollX;         final int scrollY = mScrollY;         if ((scrollX | scrollY) == 0) {//没有滚动             background.draw(canvas);         } else {             canvas.translate(scrollX, scrollY);             //最终使用了Drawable的draw             background.draw(canvas);             canvas.translate(-scrollX, -scrollY);         }     }
 | 
第二步,保存图层,执行渐变操作
主要调用canvas.saveLayer方法,不多说
第三步,绘制内容
| 1 2 3
 |        if (!dirtyOpaque)         onDraw(canvas);
 | 
而onDraw又是个空方法,因为具体绘制什么东西还得具体的view说了算
第四步,绘制所有的子view
| 1 2 3 4 5 6 7 8 9 10 11 12
 |           dispatchDraw(canvas);               * Called by draw to draw the child views. This may be overridden      * by derived classes to gain control just before its children are drawn      * (but after its own view has been drawn).      * @param canvas the canvas on which to draw the view      */     protected void dispatchDraw(Canvas canvas) {     }
 | 
这里看到dispatchDraw是个空方法,注释里面说,如果包含子view就需要去重写这个方法,去viewgroup里面看看。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 | @Override    protected void dispatchDraw(Canvas canvas) {        ......        final int childrenCount = mChildrenCount;        final View[] children = mChildren;        ......        for (int i = 0; i < childrenCount; i++) {            ......            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {                more |= drawChild(canvas, child, drawingTime);            }        }        ......                if (mDisappearingChildren != null) {            ......            for (int i = disappearingCount; i >= 0; i--) {                ......                more |= drawChild(canvas, child, drawingTime);            }        }        ......    }
 | 
还是去遍历所有的子view,然后去调用view.draw方法
第五步,绘制渐变效果并且恢复图层
这个和第二部相对应,不说了
第六步,绘制view的滚动条
也不分析了,所以其实所有的 view都是有滚动条的
到这里,整个view就差不多绘制完成了。
View的invalidate
如果去查一查view的源码,你会发现这个方法出现的特别多,我们这个方法是用来在主线程中调用去重绘view的,但是为什么会出现这么多次,而且它是怎么去实现重绘的呢,而且开头的时候还有一个问题没有解决,简单的看一下。
view有多个重载的invalidate,但是最终都走到了invalidateInternal方法,看一下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14
 | void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,             boolean fullInvalidate) {         ......             final AttachInfo ai = mAttachInfo;             final ViewParent p = mParent;             if (p != null && ai != null && l < r && t < b) {                 final Rect damage = ai.mTmpInvalRect;                                  damage.set(l, t, r, b);                                  p.invalidateChild(this, damage);             }             ......     }
 | 
这里主要是p.invalidateChild(this, damage);这个方法,
View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。
而ViewParent是个接口,viewgroup和ViewRootImpl都实现了这个接口,所以先去viewgroup看看:
| 1 2 3 4 5 6 7 8 9 10 11
 | public final void invalidateChild(View child, final Rect dirty) {        ViewParent parent = this;        final AttachInfo attachInfo = mAttachInfo;        ......        do {            ......                        parent = parent.invalidateChildInParent(location, dirty);            ......        } while (parent != null);    }
 | 
这里进行了了循环,最终还是去了ViewRootImpl,看看
| 1 2 3 4 5 6 7 8
 | @Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) {     ......          scheduleTraversals();     ......     return null; }
 | 
这里直接返回Null,退出上面的循环,而scheduleTraversals这个方法,最终会调用最初我们看到的performTraversals方法进行重新绘制。但是肯定是不会走最初的流程,经过一些判断之后只会去重绘需要重绘的地方。还是贴张图(来自工匠若水)
 ,
,
常见的引起invalidate方法操作的原因主要有:
直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。
触发setSelection方法。请求重新draw,但只会绘制调用者本身。
触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。
触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。
触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。
最后再来去解释一下最初的那个view绘制入口的问题,还是接上一篇,在setContentView方法里面有这个mContentParent.addView(view, params);而在addView方法里面有这么一句话:
| 1 2 3 4 5
 | public void addView(View child, int index, LayoutParams params) {         ......         invalidate(true);         ......     }
 | 
这下明白了吧。
参考:
 Android应用层View绘制流程与源码分析