文章目录
- Android视图构成
- setContentView流程
- Andorid 的View绘制
- 一、起点:performTraversals() 方法
- 二、View绘制流程第一步measure分析(API 23)
- 普通View的测量
- ViewGroup测量所有子View
- 总结
- 三、View绘制流程第二步layout过程分析(API 23)
- 总结
- 四、View绘制流程第三步draw过程分析(API 23)
- 第一步 画背景(Draw the background)
- 第三步 绘制视图内容(Draw view's content)
- 第四步 绘制子View(Draw children)
- 第六步 绘制装饰(例如滚动条)(Draw decorations (scrollbars for instance))
- 总结
- onAttachedToWindow() 和 onDetachedFromWindow()
Android视图构成
如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是DecorView,这个DecorView就是存放布局文件,里面有TitleActionBar和我们setContentView传入进去的layout布局文件
- Window类时一个抽象类,提供绘制窗口的API
- PhoneWindow是继承Window的一个具体的类,该类内部包含了一个DecorView对象,该DecorView对象时所有应用窗口(Activity界面)的根View
- DecorView继承FrameLayout,里面 id = content 就是我们传入的布局视图
更具面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装的位置。
setContentView流程
setContentView整个过程主要是如何把Activity布局文件或者java的View添加窗口里,重点概述如下:
- 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图
- 依据Feature等style
theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout) - 将Activity的布局文件添加至id为content的FrameLayout内
- 当setContentView设置显示OK以后会回调Activity的onContentChange方法。Activity的各种View的findViewById方法等都可以放到该方法中,系统会帮忙回调
Andorid 的View绘制
一、起点:performTraversals() 方法
视图绘制的起点在_ViewRootImpl_ 类的 _performTraversals()_方法,该方法完成的工作主要是:根据之前的状态,判读是否重新计算测试视图的大小(measure),是否重新放置视图位置(layout)和是否重新重绘视图(draw),部分源码如下:
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 be
7 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
8 ......
9 performLayout(lp, desiredWindowWidth, desiredWindowHeight);
10 ......
11 performDraw();
12 ......
13 }
第2,3行代码调用getRootMeasureSpec方法生成对应宽高,下面是getRootMeasureSpec的源码:
1 /**
2 * Figures out the measure spec for the root view in a window based on it's
3 * layout params.
4 *
5 * @param windowSize
6 * The available width or height of the window
7 *
8 * @param rootDimension
9 * 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,生成的MeasureSpec中SpecMode为MeasureSpec.EXACTLY,SpecSize则为窗口的可用尺寸
回到performTraversals() 方法中:
7,9,11分别执行了performMeasure(childWidthMeasureSpec, childHeightMeasureSpec), performLayout(lp,desiredWindowWidth, 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 }
performMeasure方法最核心的是第4行 调用了mView的measure方法。
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 }
performLayout 方法通过5,7 行代码发现其实也是调用了mView的layout方法
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 }
performDraw最终调用的也是mView的draw方法,mView就是DecorView,我们知道DecorView是FrameLayout,FrameLayout继承自ViewGroup,ViewGrop继承自View,所以最终都会调用 View类中measure,layout,draw方法。
所以view绘制主要包括三个方面:
- measure 测量组件本身的大小
- layout 确定组件在视图中的位置
- draw 根据位置和大小,将组件画下来
这三个子阶段可以用下图来描述:
二、View绘制流程第一步measure分析(API 23)
普通View的测量
1 /**
2 * <p>
3 * This is called to find out how big a view should be. The parent
4 * supplies constraint information in the width and height parameters.
5 * </p>
6 *
7 * <p>
8 * The actual measurement work of a view is performed in
9 * {@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 修饰,所以这个方法不能被子类重写。
实际的测量是在 24行 onMeasure方法进行,所以在View的普通子类中需要重写onMeasure方法来实现自己的测量逻辑。
对于普通View,调用View类的onMeasure() 方法来进行实际的测量工作即可,当然我们也可以重载onMeasure并调用setMeasuredDimension 来设置任意大小的布局。
接下来我们来看看默认情况下 onMeasure方法都做了什么,源码如下:
1 /**
2 * <p>
3 * Measure the view and its content to determine the measured width and the
4 * measured height. This method is invoked by {@link #measure(int, int)} and
5 * should be overridden by subclasses to provide accurate and efficient
6 * 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,setMeasureDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth 和 mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWith 和 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,如果设置了背景,则取最大值。如果都没有设置,则返回0。
接下来看getDefault方法源码:
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 }
getDefault返回由 上面讲到的 getSuggestedMiniXXX方法获取到的size以及父类传递过来的measureSpec共同决定。可以看到如果specMode等于AT_MOST或EXACTLY 就返回specSize(父类传递过来的),这也是系统默认的规格。
到此为止,普通的View(非ViewGroup)的测量就基本讲解完了。但是ViewGroup这个容器类的布局是怎么测量其内部每个子View的呢?
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 以及子类child调用getLayoutParams() 方法生成子类自己具体的测量规格。(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.rightMargin
8 + 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相比最重要的区别是 measureChildWithWargins 额外将具体子View的LayoutParams参数的margin也作为参数来生成测量规格。
measureChild 与 measureChildWithWargins 都调用了 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的size
4 //父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的LayoutPaarmas生成了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类提供了measureChildren、measureChild和measureChildWithMargins方法,以供容器类布局测量自身子View使用
- 使用View的getMeasuredWidth() 和 getMeasureHeight()
方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值,只有onMeasure流程完成后mMeasuredWidth
与 mMeasureHeight 才会被赋值 - View的布局大小是由父View和子View共同决定的。我们平时设置的宽高可以理解为期望的大小,具体大小还要结合父类大小来确定。
最后附上View绘制流程图:
三、View绘制流程第二步layout过程分析(API 23)
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 @Override
2 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时候说过mMeasuredHeight 与 mMeasuredWidth 只有测量过程完成才会被赋值,所以只有测量过程完成调用getMeasuredWidth() 、getMeasuredHeight() 才会获取正确的值。
而 getWidth()、getHeight() 只有在Layout过程完成时mLeft、mRight、mTop、mBottom才会被赋值,调用才会获取正确返回值,所以二者一个在view时调用,一个在layout时调用。
继续看view中layout源码第7行,如果changed为true,也就是说view的位置发生了变化,或者标记为PFLAG_LAYOUT_REQUIRED 则进入判断执行onLayout方法。
我们继续看View中onLayout方法源码:
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
}
发现是个空方法。
对比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的摆放逻辑。
我们来看看具体子类,我们选取FrameLayout,其onLayout源码如下:
1 @Override
2 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,mRight,mTop,mBottom,这些值都是相对于父View来说的。 - 使用View的getWidth() 和 getHeight()
方法来获取View测量的宽高,必须保持这两个方法在onLayout流程之后被调用才能返回有效值。
四、View绘制流程第三步draw过程分析(API 23)
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 executed
6 * in the appropriate order:
7 *
8 * 1. Draw the background
9 * 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步:
1、画背景
2、必要时,保存画布的层以准备渐变。
3、绘制视图内容
4、画孩子
5、如果需要,绘制渐变边缘并恢复图层。
6、绘制装饰(例如滚动条)
再由23 行提示大部分情况下跳过第2步,第5步,所以我们着重分析其余四步。
第一步 画背景(Draw the background)
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过程摆放的位置绘制出来
第三步 绘制视图内容(Draw view’s content)
第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要绘制的内容是不同的,所以子类需要实现这个方法来绘制自身的内容。
第四步 绘制子View(Draw children)
第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中有没有实现这个方法,源码如下:
1 @Override
2 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方法来完成自身的绘制。
第六步 绘制装饰(例如滚动条)(Draw decorations (scrollbars for instance))
40行 执行绘制装饰逻辑,这一部分只是绘制一些装饰物,比如 ScrollBar,这部分就不分析了,也不是重点.
到这里,View的主要绘制流程我们就分析完了,也不复杂。
但是,我们在执行第一步和第三步时,都有个if判断(if(!dirtyOpaque)),也就是说只有判断成立才会执行绘制背景和自身内容。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子类)默认情况下不绘制背景和自身内容呢?答案是为了性能。
总结
- 容器类布局需要递归绘制其所有包的所有子View
- View中 onDraw默认是空方法,需要子类自己实现来完成自身内容的绘制
- 容器类布局默认是不会调用onDraw方法的,我们可以为其设置背景或者调用setWillNotDraw(flase) 方法来使其主动调用onDraw方法
流程图:
onAttachedToWindow() 和 onDetachedFromWindow()
onAttachedToWindow() 是第一次onDraw前调用。也就是我们写的View还没有绘制出来时调用,但只会调用一次。
onDetachedFromWinodow() 销毁资源(即销毁View)之后调用
View状态的保持:onSaveInstanceState() 方法