Android之View绘制流程源码分析
版权声明:本文出自汪磊的博客,转载请务必注明出处。
对于稍有自定义View经验的安卓开发者来说,onMeasure,onLayout,onDraw这三个方法都不会陌生,起码多少都有所接触吧。
在安卓中,一个View显示到屏幕上基本上都是经过测量,摆放,绘制这三个过程才显示出来,那么这三个过程到底是怎么执行的呢?本文与大家一起探讨一下安卓中View的绘制流程。
一,View树绘制流程开始的地方(API23)
对一个布局进项测量,摆放,绘制肯定要有开始的地方吧,这里就直接跟大家说了,View绘制流程开始的地方是ViewRootImpl类的performTraversals()方法(至于为什么是这里不是本篇重点,后续有时间写一篇针对这里的文章说明一下),接下来我们看下performTraversals()方法(此方法过长,只列出重要逻辑代码)
1 private void performTraversals() {2 ......3 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);4 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);5 ......6 // Ask host how big it wants to be7 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);8 ......9 performLayout(lp, desiredWindowWidth, desiredWindowHeight); 10 ...... 11 performDraw(); 12 ...... 13 }
第3,4行代码调用getRootMeasureSpec方法生成对应宽高,我们先看下getRootMeasureSpec都做了什么,源码如下:
1 /**2 * Figures out the measure spec for the root view in a window based on it's3 * layout params.4 *5 * @param windowSize6 * The available width or height of the window7 *8 * @param rootDimension9 * The layout params for one dimension (width or height) of the 10 * window. 11 * 12 * @return The measure spec to use to measure the root view. 13 */ 14 private static int getRootMeasureSpec(int windowSize, int rootDimension) { 15 int measureSpec; 16 switch (rootDimension) { 17 18 case ViewGroup.LayoutParams.MATCH_PARENT: 19 // Window can't resize. Force root view to be windowSize. 20 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); 21 break; 22 case ViewGroup.LayoutParams.WRAP_CONTENT: 23 // Window can resize. Set max size for root view. 24 measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); 25 break; 26 default: 27 // Window wants to be an exact size. Force root view to be that size. 28 measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 29 break; 30 } 31 return measureSpec; 32 }
先说一下MeasureSpec这个概念:也叫测量规格,MeasureSpec是一个32位整数,由SpecMode和SpecSize两部分组成,其中,高2位为SpecMode,低30位为SpecSize。SpecMode为测量模式,SpecSize为相应测量模式下的测量尺寸。
View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams结合父View的MeasureSpec生成(普通View的MeasureSpec是由其父类ViewGroup生成的,后面会详细讲到)。
SpecMode的取值可为以下三种:
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View对子View的大小不做限制,完全由子View自己决定;
getRootMeasureSpec方法就是生成根视图的
MeasureSpec,还记得我们上一篇《Android之View绘制流程开胃菜---setContentView(...)详细分析》中分析的吗,平时我们自己写的布局都是被添加到DecorView中id为content的布局中的,
这里传入进来的windowSize参数是window的可用宽高信息,rootDimension宽高参数均为MATCH_PARENT。
我们上面说普通View的MeasureSpec是由其父类ViewGroup生成的,但是根视图DecorView是没有父类的,所以
getRootMeasureSpec就是给根视图生成测量规格的,生成的MeasureSpec中SpecMode为MeasureSpec.EXACTLY
,SpecSize则为窗口的可用尺寸。
回到performTraversals()方法中:
3,4行代码分别生成宽高的测量规格
7,9,11行代码分别执行performMeasure(childWidthMeasureSpec, childHeightMeasureSpec), performLayout(lp, desiredWindowWidth, desiredWindowHeight),performDraw();
方法,我们看下这三个方法源码:都经过简化处理
1 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { 2 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); 3 try { 4 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 5 } finally { 6 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 7 } 8 }
1 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, 2 int desiredWindowHeight) { 3 4 ... 5 final View host = mView; 6 7 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 8 ... 9 }
1 private void performDraw() { 2 ... 3 draw(fullRedrawNeeded); 4 ... 5 }
1 private void draw(boolean fullRedrawNeeded) { 2 3 ... 4 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { 5 return; 6 } 7 ... 8 }
1 private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, 2 boolean scalingRequired, Rect dirty) { 3 ... 4 mView.draw(canvas); 5 ... 6 return true; 7 }
performMeasure方法最核心的是第4行调用mView的measure方法。performLayout方法通过5,7行代码发现其实也是调用的mView的layout方法。performDraw最终调用的也是调用的mView的draw方法。
上面的mView就是DecorView,我们知道DecorView是FrameLayout,FrameLayout继承自ViewGroup,ViewGroup继承自View,所以最终都会调用View类中measure,layout,draw方法。
实际上View的绘制流程可以分为三个阶段:
- measure: 判断是否需要重新计算View的大小,需要的话则计算;
- layout: 判断是否需要重新计算View的位置,需要的话则计算;
- draw: 判断是否需要重新绘制View,需要的话则重绘制。
大体流程如图:
二,View绘制流程第一步measure过程分析(API23)接下来我们看下View中的measure源码:简化处理
1 /**2 * <p>3 * This is called to find out how big a view should be. The parent4 * supplies constraint information in the width and height parameters.5 * </p>6 *7 * <p>8 * The actual measurement work of a view is performed in9 * {@link #onMeasure(int, int)}, called by this method. Therefore, only 10 * {@link #onMeasure(int, int)} can and must be overridden by subclasses. 11 * </p> 12 * 13 * 14 * @param widthMeasureSpec Horizontal space requirements as imposed by the 15 * parent 16 * @param heightMeasureSpec Vertical space requirements as imposed by the 17 * parent 18 * 19 * @see #onMeasure(int, int) 20 */ 21 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 22 ... 23 // measure ourselves, this should set the measured dimension flag back 24 onMeasure(widthMeasureSpec, heightMeasureSpec); 25 ... 26 }
注释已经给出大体描述:这个被调用用来测算出view大小,并且其父类提供了约束信息widthMeasureSpec与heightMeasureSpec。
我们发现measure方法被final修饰,所以这个方法不能被子类重写。
实际的测量是在onMeasure方法进行,所以在View的普通子类中需要重写onMeasure方法来实现自己的测量逻辑。
对于普通View,调用View类的onMeasure()方法来进行实际的测量工作即可,当然我们也可以重载onMeasure并调用setMeasuredDimension来设置任意大小的布局。
接下来我们看下默认情况下View类中onMeasure方法都做了什么,源码如下;
1 /**2 * <p>3 * Measure the view and its content to determine the measured width and the4 * measured height. This method is invoked by {@link #measure(int, int)} and5 * should be overridden by subclasses to provide accurate and efficient6 * measurement of their contents.7 * </p>8 *9 * <p> 10 * <strong>CONTRACT:</strong> When overriding this method, you 11 * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the 12 * measured width and height of this view. Failure to do so will trigger an 13 * <code>IllegalStateException</code>, thrown by 14 * {@link #measure(int, int)}. Calling the superclass' 15 * {@link #onMeasure(int, int)} is a valid use. 16 * </p> 17 * 18 * <p> 19 * The base class implementation of measure defaults to the background size, 20 * unless a larger size is allowed by the MeasureSpec. Subclasses should 21 * override {@link #onMeasure(int, int)} to provide better measurements of 22 * their content. 23 * </p> 24 * 25 * <p> 26 * If this method is overridden, it is the subclass's responsibility to make 27 * sure the measured height and width are at least the view's minimum height 28 * and width ({@link #getSuggestedMinimumHeight()} and 29 * {@link #getSuggestedMinimumWidth()}). 30 * </p> 31 * 32 * @param widthMeasureSpec horizontal space requirements as imposed by the parent. 33 * The requirements are encoded with 34 * {@link android.view.View.MeasureSpec}. 35 * @param heightMeasureSpec vertical space requirements as imposed by the parent. 36 * The requirements are encoded with 37 * {@link android.view.View.MeasureSpec}. 38 * 39 * @see #getMeasuredWidth() 40 * @see #getMeasuredHeight() 41 * @see #setMeasuredDimension(int, int) 42 * @see #getSuggestedMinimumHeight() 43 * @see #getSuggestedMinimumWidth() 44 * @see android.view.View.MeasureSpec#getMode(int) 45 * @see android.view.View.MeasureSpec#getSize(int) 46 */ 47 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 48 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 49 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 50 }
这个方法看注释就已经大体明白了,简单翻译一下吧:这个方法用来测量view以及自身内容来决定宽高,子类应该重写这个方法提供更精确更高效的测量的内容。当重写这个方法的时候子类必须调用setMeasuredDimension(int, int)来存储已经测量出来的宽高。
我们看到系统默认的onMeasure方法只是直接调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。
接下来我们看看设置的默认View宽高,默认宽高都是通过getDefaultSize方法来获取的,而getDefaultSize又调用了getSuggestedMinimumXXX方法,我们先看下getSuggestedMinimumXXX方法:
1 protected int getSuggestedMinimumHeight() { 2 return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); 3 }
1 protected int getSuggestedMinimumWidth() { 2 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); 3 }
mMinHeight或mMinWidth就是我们设置的android:minHeight或android:minWidth参数。如果我们没有设置背景则直接返回mMinHeight或mMinWidth,如果设置了背景,则返回miniXXX属性与mBackground二者中较大者。如背景以及miniXXX属性都没设置呢?那就返回0了。接下来再看getDefaultSize方法源码:
1 public static int getDefaultSize(int size, int measureSpec) {2 int result = size;3 int specMode = MeasureSpec.getMode(measureSpec);4 int specSize = MeasureSpec.getSize(measureSpec);5 6 switch (specMode) {7 case MeasureSpec.UNSPECIFIED:8 result = size;9 break; 10 case MeasureSpec.AT_MOST: 11 case MeasureSpec.EXACTLY: 12 result = specSize; 13 break; 14 } 15 return result; 16 }
getDefaultSize返回值由上面讲到的getSuggestedMinimumXXX方法获取的Size以及父类传递过来的measureSpec共同决定。可以看到如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的规格。到此为止,普通View(非ViewGroup)的测量就基本讲完了。但是ViewGroup这种容器类布局是怎么测量其内每个子View的呢?ViewGroup容器类布局大部分情况下是用来嵌套具体子View的,所以需要负责其子View的测量,在ViewGroup中定义了measureChildren(int widthMeasureSpec, int heightMeasureSpec)measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec)以及measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)三个方法来供其子类调用对具体子View进行测量。measureChildren,measureChild源码如下:
1 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {2 final int size = mChildrenCount;3 final View[] children = mChildren;4 for (int i = 0; i < size; ++i) {5 final View child = children[i];6 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {7 measureChild(child, widthMeasureSpec, heightMeasureSpec);8 }9 } 10}
1 protected void measureChild(View child, int parentWidthMeasureSpec,2 int parentHeightMeasureSpec) {3 final LayoutParams lp = child.getLayoutParams();4 5 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,6 mPaddingLeft + mPaddingRight, lp.width);7 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,8 mPaddingTop + mPaddingBottom, lp.height);9 10 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 11}
看到了吧,measureChildren只是循环调用measureChild方法,而measureChild方法中会根据父类提供的测量规格parentXXXMeasureSpec一级子类自己LayoutParams调用getChildMeasureSpec方法生成子类自己具体的测量规格。(getChildMeasureSpec稍后会具体分析)
接下来我们看下measureChildWithMargins方法源码:
1 protected void measureChildWithMargins(View child,2 int parentWidthMeasureSpec, int widthUsed,3 int parentHeightMeasureSpec, int heightUsed) {4 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();5 6 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,7 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin8 + widthUsed, lp.width);9 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 10 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin 11 + heightUsed, lp.height); 12 13 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 14 }
与measureChild相比最主要的区别就是measureChildWithMargins额外将具体子View LayoutParams参数的margin也当作参数来生成测量规格。
measureChild与measureChildWithMargins均调用了getChildMeasureSpec方法来生成具体测量规格,接下来我们重点看下这个方法:
1 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {2 int specMode = MeasureSpec.getMode(spec);//获取父View的mode 3 int specSize = MeasureSpec.getSize(spec);//获取父View的size4 //父View的size减去padding与0比较取其大,specSize - padding得到的值是父View可以用来盛放子View的空间大小5 int size = Math.max(0, specSize - padding);6 7 int resultSize = 0;8 int resultMode = 0;9 10 switch (specMode) { 11 // Parent has imposed an exact size on us 12 case MeasureSpec.EXACTLY://父View希望子View是明确大小 13 if (childDimension >= 0) {//子View设置了明确的大小:如 10dp,20dp 14 resultSize = childDimension;//设置子View测量规格大小为其本身设置的大小 15 resultMode = MeasureSpec.EXACTLY;//mode设置为EXACTLY 16 } else if (childDimension == LayoutParams.MATCH_PARENT) {//子VIEW的宽或者高设置为MATCH_PARENT,表明子View想和父View一样大小 17 // Child wants to be our size. So be it. 18 resultSize = size;//设置子View测量规格大小为父View可用空间的大小 19 resultMode = MeasureSpec.EXACTLY;//mode设置为EXACTLY 20 } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子VIEW的宽或者高设置为WRAP_CONTENT,表明子View大小是动态的 21 // Child wants to determine its own size. It can't be 22 // bigger than us. 23 resultSize = size;//设置子View测量规格大小为父View可用空间的大小 24 resultMode = MeasureSpec.AT_MOST;//mode设置为AT_MOST,表明子View宽高最大值不能超过resultSize 25 } 26 break;27 //其余情况请自行分析 28 ......29 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 30 }
想说的注释已经给出。
上面的方法展现了根据父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的过程, 子View的LayoutParams表示了子View的期待大小。这个产生的MeasureSpec用于指导子View自身的测量。
在我们自定义View的时候一般会重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法其中的widthMeasureSpec与heightMeasureSpec参数就是父类通过getChildMeasureSpec方法生成的。一个好的自定义View会根据父类传递过来的测量规格动态设置大小,而不是直接写死其大小。
好了,到此为止View的测量过程想说的就差不多都说完了,我们稍微总结一下关键的部分;
View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的。
ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,以供容器类布局测量自身子View使用
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值,只有onMeasure流程完后mMeasuredWidth与mMeasuredHeight才会被赋值
- View的布局大小是由父View和子View共同决定的。我们平时设置的宽高可以理解为希望的大小,具体大小还要结合父类大小来确定。
最后附上View绘制流程图:相信你会理解的更加深刻:
三,View绘制流程第二步layout过程分析(API23)
performMeasure执行完,接着就会执行performLayout:
1 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, 2 int desiredWindowHeight) { 3 4 ... 5 final View host = mView; 6 7 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 8 ... 9 }
mView为根View,即DecorView,DecorView是FrameLayout的子类,最终会调用ViewGroup中layout方法。所以接下来我们看下ViewGroup中layout方法源码:
1 @Override2 public final void layout(int l, int t, int r, int b) {3 if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {4 if (mTransition != null) {5 mTransition.layoutChange(this);6 }7 super.layout(l, t, r, b);8 } else {9 // record the fact that we noop'd it; request layout when transition finishes 10 mLayoutCalledWhileSuppressed = true; 11 } 12 }
第7行代码表明又调用父类View的layout方法。所以我们看下View的layout源码,如下:
1 public void layout(int l, int t, int r, int b) { 2// l为本View左边缘与父View左边缘的距离
// t为本View上边缘与父View上边缘的距离
// r为本View右边缘与父View左边缘的距离
// b为本View下边缘与父View上边缘的距离
3 ... 4 boolean changed = isLayoutModeOptical(mParent) ? 5 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); 6 7 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { 8 onLayout(changed, l, t, r, b); 9 ... 10 }
4,5行代码主要判断View的位置是否发生变化,发生变化则changed 会为true,并且setOpticalFrame也是调用的setFrame方法
我们看下setFrame方法源码:
1 protected boolean setFrame(int left, int top, int right, int bottom) {2 boolean changed = false;3 4 5 ...6 if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {7 changed = true;8 9 ... 10 mLeft = left; 11 mTop = top; 12 mRight = right; 13 mBottom = bottom; 14 ... 15 } 16 return changed; 17 }
第6行代码分别比较之前的记录的mLeft,mRight,mTop,mBottom 与新传入的参数如果有一个不同则进入判断,将changed变量置为true,并且将新传入的参数分别重新赋值给
mLeft,mRight,mTop,mBottom,最后返回changed。
这里还有一点要说,getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别,先看一下源码;
1 public final int getMeasuredWidth() {2 return mMeasuredWidth & MEASURED_SIZE_MASK;3 }4 5 public final int getMeasuredHeight() {6 return mMeasuredHeight & MEASURED_SIZE_MASK;7 }8 9 public final int getWidth() { 10 return mRight - mLeft; 11 } 12 13 public final int getHeight() { 14 return mBottom - mTop; 15 }
在讨论View的measure过程时提到过mMeasuredWidth与mMeasuredHeight只有测量过程完成才会被赋值,所以只有测量过程完成调用getMeasuredWidth()、getMeasuredHeight()才会获取正确的值。
同样getWidth()、getHeight()只有在layout过程完成时mLeft,mRight,mTop,mBottom才会被赋值,
调用才会获取正确返回值,所以二者调用时机是不同的。
继续看View中layout源码第7行,如果changed为true,也就是说View的位置发生了变化,或者标记为PFLAG_LAYOUT_REQUIRED则进入判断执行onLayout方法。
我们继续看View中onLayout方法源码:
1 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 2 }
看到了吧,竟然是个空方法。
对比View的layout和ViewGroup的layout方法发现,View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,那么容器类View是怎么对其子View进行摆放的呢?别急,在ViewGroup中同样也有onLayout方法,源码如下;
1 /** 2 * {@inheritDoc} 3 */ 4 @Override 5 protected abstract void onLayout(boolean changed, 6 int l, int t, int r, int b);
看到了吧,还是个抽象方法,因为具体ViewGroup摆放规则不同,所以其具体子类需要重写这个方法来实现对其子View的摆放逻辑。
既然这样我们就只能分析一个继承自ViewGroup的具体子类了,我们选取FrameLayout,其onLayout源码如下:
1 @Override2 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {3 layoutChildren(left, top, right, bottom, false /* no force left gravity */);4 }5 6 void layoutChildren(int left, int top, int right, int bottom,7 boolean forceLeftGravity) {8 final int count = getChildCount();9 10 ...... 11 12 for (int i = 0; i < count; i++) { 13 final View child = getChildAt(i); 14 if (child.getVisibility() != GONE) { 15 ..... 16 17 child.layout(childLeft, childTop, childLeft + width, childTop + height); 18 } 19 } 20 }
看到了吧,onLayout方法调用layoutChildren方法,在layoutChildren方法中遍历每个子View调用其layout方法。
好了,到此Layout过程就讨论的差不多了,相比measure过程还是简单不少的,其也是递归调用逻辑。如图:
我们总结一下主要部分:
View.layout方法可被重写,ViewGroup.layout为final的不可重写,ViewGroup.onLayout为abstract的,具体ViewGroup子类必须重载来按照自己规则对子View进行摆放。
measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
测量,摆放过程都分析完了,接下来我们分析View的draw过程。
四,View绘制流程第三步draw过程分析(API23)
performMeasure, performLayout过程执行完,接下来就执行performDraw()逻辑了,ViewGroup没有重写View的draw方法,最终调用的是View中的draw方法,源码如下:
1 public void draw(Canvas canvas) {final int privateFlags = mPrivateFlags; 2 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);3 4 /*5 * Draw traversal performs several drawing steps which must be executed6 * in the appropriate order:7 *8 * 1. Draw the background9 * 2. If necessary, save the canvas' layers to prepare for fading 10 * 3. Draw view's content 11 * 4. Draw children 12 * 5. If necessary, draw the fading edges and restore layers 13 * 6. Draw decorations (scrollbars for instance) 14 */ 15 16 // Step 1, draw the background, if needed 17 int saveCount; 18 19 if (!dirtyOpaque) { 20 drawBackground(canvas); 21 } 22 23 // skip step 2 & 5 if possible (common case) 24 final int viewFlags = mViewFlags; 25 boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; 26 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; 27 if (!verticalEdges && !horizontalEdges) { 28 // Step 3, draw the content 29 if (!dirtyOpaque) onDraw(canvas); 30 31 // Step 4, draw the children 32 dispatchDraw(canvas); 33 34 // Overlay is part of the content and draws beneath Foreground 35 if (mOverlay != null && !mOverlay.isEmpty()) { 36 mOverlay.getOverlayView().dispatchDraw(canvas); 37 } 38 39 // Step 6, draw decorations (foreground, scrollbars) 40 onDrawForeground(canvas); 41 42 // we're done... 43 return; 44 } 45 ... 46 // Step 2, save the canvas' layers 47 .... 48 // Step 3, draw the content 49 if (!dirtyOpaque) onDraw(canvas); 50 51 // Step 4, draw the children 52 dispatchDraw(canvas); 53 54 // Step 5, draw the fade effect and restore layers 55 .... 56 // Step 6, draw decorations (foreground, scrollbars) 57 onDrawForeground(canvas); 58 }
5到14行注释可以看到draw过程分为6步,再看23行提示大部分情况下跳过第2,5步。所以我们着重分析其余4步。
19-21行执行第一步,绘制背景源码如下:
1 private void drawBackground(Canvas canvas) {2 final Drawable background = mBackground;3 if (background == null) {4 return;5 }6 ....7 setBackgroundBounds();8 ....9 background.draw(canvas); 10 11 } 12 13 14 void setBackgroundBounds() { 15 if (mBackgroundSizeChanged && mBackground != null) { 16 mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop); 17 mBackgroundSizeChanged = false; 18 rebuildOutline(); 19 } 20 }
只要逻辑就是获取我们在xml文件或者代码中设置的背景,然后根据layout过程摆放的位置绘制出来。
第29行执行绘制内容逻辑,源码如下:
1 /** 2 * Implement this to do your drawing. 3 * 4 * @param canvas the canvas on which the background will be drawn 5 */ 6 protected void onDraw(Canvas canvas) { 7 }
看到了吧,是一个空方法,需要具体子类自己去实现,因为每个具体View要绘制的内容是不同的,所以子类需要实现这个方法来绘制自身的内容。
第32行执行绘制子View逻辑,源码如下:
1 /** 2 * Called by draw to draw the child views. This may be overridden 3 * by derived classes to gain control just before its children are drawn 4 * (but after its own view has been drawn). 5 * @param canvas the canvas on which to draw the view 6 */ 7 protected void dispatchDraw(Canvas canvas) { 8 9 }
看到了吧,也是空方法,这个方法被用来绘制子View的,如果有子View则需要调用这个方法去绘制,我们知道一般只有容器类View才可以盛放子View,所以我们看下ViewGroup中有没有相关逻辑,在
ViewGroup
中果然实现了这个方法,源码如下:
1 @Override2 protected void dispatchDraw(Canvas canvas) {3 boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);4 final int childrenCount = mChildrenCount;5 final View[] children = mChildren;6 .......7 for (int i = 0; i < childrenCount; i++) {8 while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {9 final View transientChild = mTransientViews.get(transientIndex); 10 if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || 11 transientChild.getAnimation() != null) { 12 more |= drawChild(canvas, transientChild, drawingTime); 13 } 14 ....... 15 } 16 ...... 17 } 18 ...... 19 }
在dispatchDraw方法中遍历每个子View并且调用drawChild方法,接下来我们看下drawChild源码:
1 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 2 return child.draw(canvas, this, drawingTime); 3 }
看到了吧,最终调用每个子View的draw方法来完成自身的绘制。
接下来40行执行onDrawForeground逻辑,这一部分只要绘制一些装饰物,比如ScrollBar。这部分就不分析了,也不是重点。
到这里,View的主要绘制流程我们也分析完了,也不复杂。
但是,但是!!!!!!细心的你有没有发现在执行第一步,第三步的时候都有个if判断(if (!dirtyOpaque)),也就是说只有判断成立才会执行绘制背景和自身内容,难道还有View不绘制自身内容吗? 这里就直接说了,ViewGroup子类默认情况下就是不执行onDraw方法的,在ViewGroup源码中的initViewGroup()方法中设置了一个标记,源码如下:
1 private void initViewGroup() { 2 // ViewGroup doesn't draw by default 3 if (!debugDraw()) { 4 setFlags(WILL_NOT_DRAW, DRAW_MASK); 5 } 6 ...... 7 }
看第二行注释也知道,ViewGroup默认情况下是不会draw的。
第四行调用setFlags方法设置标记WILL_NOT_DRAW,
我们在回到View中draw方法看第2行代码:
1 final int privateFlags = mPrivateFlags;2 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && 3 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
setFlags方法就是对View中mPrivateFlags值进行相应改变,我们设置标记WILL_NOT_DRAW那么dirtyOpaque得到的值就为true,从而if (!dirtyOpaque)不成立,也就不会执行onDraw方法。
估计这点很多同学有疑问ViewGroup默认情况下onDraw方法是不执行的???别急,动手写个小demo验证一下就是了。
布局如下:极其简单
1 <com.wanglei.clearheart.MyView xmlns:android="http://schemas.android.com/apk/res/android"2 xmlns:tools="http://schemas.android.com/tools"3 android:layout_width="match_parent"4 android:layout_height="match_parent"5 android:gravity="center"6 android:orientation="vertical"7 tools:context=".MainActivity" >8 9 10 </com.wanglei.clearheart.MyView >
MyView源码如下:同样极其简单
1 public class MyView extends ViewGroup {2 3 private Paint mPaint;4 public MyView(Context context, AttributeSet attrs) {5 super(context, attrs);6 mPaint = new Paint();7 mPaint.setColor(Color.RED);8 mPaint.setStyle(Style.STROKE);9 mPaint.setStrokeWidth(10); 10 11 } 12 13 @Override 14 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 15 16 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 17 } 18 19 @Override 20 protected void onLayout(boolean changed, int l, int t, int r, int b) { 21 // TODO Auto-generated method stub 22 23 } 24 25 @Override 26 protected void onDraw(Canvas canvas) { 27 // TODO Auto-generated method stub 28 canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, 360, mPaint); 29 } 30 }
运行程序会看到就是一个大白板,没有绘制出任何图形,那我们怎么让ViewGroup调用onDraw方法呢?
很简单View类中给我们提供了一个方法供外部调用:
1 public void setWillNotDraw(boolean willNotDraw) { 2 setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); 3 }
看到了吧,本质也是调用的setFlags方法。如果我们传入true则绘制的时候不会调用onDraw方法,传入false则使其调用onDraw方法。
我们修改MyView代码:构造方法中调用setWillNotDraw(false);
1 public MyView(Context context, AttributeSet attrs) { 2 super(context, attrs); 3 mPaint = new Paint(); 4 mPaint.setColor(Color.RED); 5 mPaint.setStyle(Style.STROKE); 6 mPaint.setStrokeWidth(10); 7 setWillNotDraw(false); 8 9 }
运行程序,会看到手机屏幕中间画出一个红色的圆。还有一种方法我们在布局中给MyView添加背景同样会达到调用onDraw方法的目的。
容器类布局(ViewGroup子类)为什么默认情况下不绘制背景和自身内容呢?答案是为了性能啊,大家想想容器类布局如果没有背景,只是用来盛放子类有必要调用onDraw方法吗?
有什么可绘制的吗?子类会自己实现onDraw方法绘制自己内容的。
接下来我们总结一下draw流程的重点:
容器类布局需要递归绘制其所包含的所有子View。
View中onDraw默认是空方法,需要子类自己实现来完成自身容内的绘制。
- 容器类布局默认情况下不会调用onDraw方法,我们可以为其设置背景或者调用setWillNotDraw(false)方法来使其主动调用onDraw方法
最后附上draw流程图:
好了,到此本篇就该结束了,用了很长的篇幅来探讨View的绘制流程,希望对大家有用,废话就不多说了,咱们下篇见。
转载于:https://www.cnblogs.com/leipDao/p/7573803.html
Android之View绘制流程源码分析相关推荐
- Android usb广播 ACTION_USB_DEVICE_ATTACHED流程源码分析
整体流程图 大概意思就是UsbHostManager启动监控线程,monitorUsbHostBus会调用usb_host_run函数(使用inotify来监听USB设备的插拔)不停的读取bus总线, ...
- android系统加载主题的流程,详解Android布局加载流程源码
一.首先看布局层次 看这么几张图 我们会发现DecorView里面包裹的内容可能会随着不同的情况而变化,但是在Decor之前的层次关系都是固定的.即Activity包裹PhoneWindow,Phon ...
- Activity启动流程源码分析(基于Android N)
Activity启动流程源码分析 一个Activity启动分为两种启动方式,一种是从Launcher界面上的图标点击启动,另一种是从一个Activity中设置按钮点击启动另外一个Activity.这里 ...
- Activity启动流程源码分析-浅析生命周期函数
源码分析 接着上一篇 Activity启动流程源码分析-setContentView源码阅读 的讲解,本节介绍一下Activity的生命周期函数何时被调用 要看Activity的生命周期函数何时被调用 ...
- OkHttp原理流程源码分析
OkHttp已经是非常流行的android客户端的网络请求框架,我其实在项目中使用也已经好几年了,之前一直把重心放在如何快速的搞定业务上.迭代的效率上,这一点来讲,对于一个公司优秀员工是没有毛病的.但 ...
- 二次开发:flowable审批流程实践与创建流程源码分析
二次开发:flowable审批流程实践与创建流程源码分析 上一篇已经描述了基于开源项目https://doc.iocoder.cn/的flowable的快速开发,创建了一个租户,创建了用户和相应的岗位 ...
- 【源码分析】storm拓扑运行全流程源码分析
[源码分析]storm拓扑运行全流程源码分析 @(STORM)[storm] 源码分析storm拓扑运行全流程源码分析 一拓扑提交流程 一stormpy 1storm jar 2def jar 3ex ...
- Android服务查询完整过程源码分析
Android服务注册完整过程源码分析中从上到下详细分析了Android系统的服务注册过程,本文同样针对AudioService服务来介绍Android服务的查询过程. 客户端进程数据发送过程 pri ...
- Android服务注册完整过程源码分析
前面从不同片段分析了Android的Binder通信机制,本文结合前面介绍的内容,对整个Android的Binder通信过程进行一次完整的分析.分析以AudioService服务的注册过程为例. 由于 ...
最新文章
- Android自动化测试框架
- Shell图形化监控网络流量
- 自定义语言的实现——解释器模式
- html5 canvas 版 hello world! 暨haXe简介
- android opencv 获取小图在大图的坐标_Android开发—基于OpenCV实现相机实时图像识别跟踪...
- Web开发静态资源处理---SpringBoot
- 介绍 Spring 3.1 M1 中的缓存功能
- leetcode 424. 替换后的最长重复字符(滑动窗口)
- centos 7 nginx hhvm mysql_CentOS 7 编译安装 HHVM 3.4.1
- ReviewBoard安装和配置札记
- SES 之全局搜索小记
- SpringContext扩展BeanFactory
- NRF52840 USB串口例程
- 安装dbVisualizer报错:the java_home environment viariable does not point to a working 32-bit JDK OR JRE
- 服务器输出字符流数据到浏览器 使用println浏览器无显示
- 【高等数学笔记】变限积分求导问题
- ant design vue table 高度自适应_对比1万2千个Vue.js开源项目发现最实用的 TOP45!火速拿来用!...
- 云重磅 | 阿里巴巴平均每天纳税超1.4亿;谷歌、Face book与AWS将共建美欧新海底电缆;阿里发布谣言粉碎机:1秒辨真伪...
- Java岗最全面试攻略,吃透这些技术栈Offer拿到手软
- registrar计算机词汇,HR常用短语英语词汇表(转载)
热门文章
- jquery treeview 树形插件
- 凤凰网制作的 “中国挺住” 桌面与MSN,QQ头像下载
- 面向对象编程概念_如何向6岁的孩子解释面向对象的编程概念
- ai css 线条粗细_如何训练AI将您的设计模型转换为HTML和CSS
- c# 数据可视化_#OpenVisConf上的22位数据可视化从业者的10点收获
- linux程序计数器,如何在C中打印程序计数器的确切值
- oracle的表几种连接比较,几种表连接方式的使用场景
- 零基础怎么学习Java?
- 使用reflector对.NET反编译
- 腾讯开源基于 mmap 的高性能 key-value 组件 MMKV