这次来说一说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绘制流程与源码分析