1、前言
了解View的绘制过程以及工作原理是自定义View的基础,也是各大面试的高频考点,总之了解并掌握这块知识非常重要。
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过了measure(测量)、layout(布局)、draw(绘制)最终把View绘制出来。
- measure:负责测量View的长和宽
- layout:负责View在父容器中的显示位置
- draw:负责将View绘制在屏幕上
让我们来看一下performTraversals的源码:
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
因为源码实在是太长了,我们截取了重要的来看,里面包含了测量、布局、和绘制。下面我们绘制一个流程图更直观显示:
接下来我们根据View绘制的三大流程来具体分析
2、Measure(测量)过程源码探究
2-1:MeasureSpec介绍
可以翻译成测量规则,代表一个32为的int值,高2位代表SpecMode是指测量模式,低30为代表SpecSize是指在这种测量模式下的大小。下面我们去看一下代码:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//SpecMode的一种类型
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//SpecMode的一种类型
public static final int EXACTLY = 1 << MODE_SHIFT;
//SpecMode的一种类型
public static final int AT_MOST = 2 << MODE_SHIFT;
//通过SpaceSize和SpaceMode来打包成一个整形的MeasureSpec,来避免过多的
//对象内存分配
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取SpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//获取SpecSize
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
下面介绍一下三种类型的SpecMode:
- EXACTLY:父容器检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的match_parent和具体的数值这两种模式。
- AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体什么值要看View的具体实现。它对应LayoutParams中的wrap_content。
- UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
2-2:DecorView的MeasureSpec创建过程源码探究
绘制前首先要获取根View的MeasureSpec,其实是为了后面测量ViewGroup和View,我们通过源码来分析:
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
}
其中mWidth和mHeight分别为屏幕的宽和高,我们继续看getRootMeasureSpec源码:
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;
}
通过上面的代码Decorview的MeasureSpec的产生过程也就明确了,这里不再解释了。
2-3:View的Measure源码探究
View中measure过程相关的方法主要有三个:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
View的Measure方法调用onMeasure方法,onMeasure方法会调用setMeasuredDimension方法为View的mMeasuredWidth和mMeasuredHeight进行赋值,一旦赋值完成就意味着View的测量结束,下面我们通过源码具体探究
首先来看measure方法:
//final方法,子类不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
看上面代码,只贴出来只要的部分,为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measure方法是final的,不允许重载,所以View子类只能通过重载onMeasure来实现自己的测量逻辑。
我们继续来看onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我们可以看见onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。
接下来我们看一下getDefaultSize源码:
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;
}
看getDefaultSize方法逻辑很简单,这里分两种情况:
- SpecMode为AT_MOST和EXACTLY时:
getDefaultSize返回的大小就是MeasureSpec中的SpecSize,而此SpecSize就是view测量后的大小。 - SpecMode为UNSPECIFIED情况:
一般用于系统内部测量过程,在这种情况下,View的大小为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值
我们来看一下getSuggestedMinimumWidth的源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这里对getSuggestedMinimumWidth方法的逻辑解释一下:如果view没有设置背景,那么返回android:minWidth这个属性所指定的值。如果view设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。
接着我们回过头来看setMeasuredDimension这个函数的源码:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
调用了setMeasuredDimensionRaw:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
看到了吧,这里设置了View宽高的测量值,赋值结束也就意味着measure过程结束。
2-4:ViewGroup的Measure源码探究
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。主要的方法有:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)
measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。
首先我们来看measureChildren源码:
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);
}
}
}
很显然,ViewGroup在measure时会对每一个子元素进行measure。
接下来看一下比较复杂一点的measureChildWithMargins源码:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子元素的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//调整MeasureSpec
//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
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);
//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给子元素的measure方法进行测量。
我们接着看getChildMeasureSpec源码:
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec很简单,可以看见,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension。
自此,measure(测量)过程分析完毕。我们继续………….
3、Layout(布局)过程源码探究
现在让我们继续来探究layout过程,Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用layout方法,在layout方法中onLayout方法又会被回调。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。
回到源头,看函数调用:
private void performTraversals() {
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
}
上面代码,左、上都为0,右下是我们刚才measure过程测出来的宽和高。
我们来看一下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);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
我们看到,ViewGroup的layout其实还是调用了view的layout,让我们去看一下view的layout的源码:
public void layout(int l, int t, int r, int b) {
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;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
可以看到,首先会通过setFrame方法来设定view的四个顶点额位置,即初始化mLeft、mTop、mBottom和mRight,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了。接着会调用onLayout方法,这个方法用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。
我们来验证一下上述说法:
看一下ViewGroup的onLayout方法源码:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
果然是个抽象方法,具体实现跟具体子类有关。
继续看一下View的onLayout源码:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
可以看到竟然是一个空方法,所以验证了我们上述的说法。
既然这样那我们只能分析一个现有的继承ViewGroup的控件了,就拿LinearLayout来说吧,如下是LinearLayout中onLayout的一些代码:
@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);
}
}
LinearLayout的layout过程是分Vertical和Horizontal的,这个就是xml布局的orientation属性设置的,我们为例说明ViewGroup的onLayout重写一般步骤就拿这里的VERTICAL模式来解释吧,如下是layoutVertical方法源码:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
//计算父窗口推荐的子View宽度
final int width = right - left;
//计算父窗口推荐的子View右侧位置
int childRight = width - mPaddingRight;
//child可使用空间大小
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
//获取Gravity属性设置
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//依据majorGravity计算childTop的位置值
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
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) {
//LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//获取子View的LayoutParams
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);
//依据不同的absoluteGravity计算childLeft位置
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;
//通过垂直排列计算调运child的layout设置child的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
从上面分析的ViewGroup子类LinearLayout的onLayout实现代码可以看出,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子View的具体位置。
到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别(上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效)。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。看一下源码:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
public final int getLeft() {
return mLeft;
}
public final int getRight() {
return mRight;
}
public final int getTop() {
return mTop;
}
public final int getBottom() {
return mBottom;
}
从getWidth和getHeight的源码再结合mLeft、mRight、mTop和mBottom这四个变量的赋值过程来看,getWidth方法返回的刚好就是View的测量宽度。
在View默认实现中,View的测量宽高和最终的宽高是相等的,只不过测量宽高形成于measure过程,而最终的宽高形成于View的layout过程,即两者赋值时机不同,测量宽高的赋值较早一些。
View的layout介绍完了,下面作如下总结:
- View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
- measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
- 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的
- 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
自此,layout(布局)过程分析完毕。我们继续………….
4、Draw(绘制)过程源码探究
draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和layout()之后,接着执行view.draw():
private void performTraversals() {
......
final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
}
这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。
由于ViewGroup没有重写View的draw方法,所以如下直接从View的draw方法开始分析:
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)
*/
// Step 1, draw the background, if needed
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
看见整个View的draw方法很复杂,但是源码注释也很明显。从注释可以看出整个draw过程分为了6步。源码注释说(”skip step 2 & 5 if possible (common case)”)第2和5步可以跳过,所以我们接下来重点剩余四步。如下:
第一步,对View的背景进行绘制:
可以看见,draw方法通过调运drawBackground(canvas);方法实现了背景绘制。我们来看下这个方法源码,如下:
private void drawBackground(Canvas canvas) {
//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
final Drawable background = mBackground;
......
//根据layout过程确定的View位置来设置背景的绘制区域
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//调用Drawable的draw()方法来完成背景的绘制工作
background.draw(canvas);
......
}
第三步,对View的内容进行绘制。
可以看到,这里去调用了一下View的onDraw()方法,所以我们看下View的onDraw方法(ViewGroup也没有重写该方法),如下:
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
可以看见,这是一个空方法。因为每个View的内容部分是各不相同的,所以需要由子类去实现具体逻辑。
第四步,对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制:
我们来看下View的draw方法中的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) {
}
看见没有,View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他,所以我们有必要看下ViewGroup的dispatchDraw方法源码(这也就是刚刚说的对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制的原因,因为如果是View调运该方法是空的,而ViewGroup才有实现),如下:
@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);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可以看见drawChild()方法调运了子View的draw()方法。所以说ViewGroup类已经为我们重写了dispatchDraw()的功能实现,我们一般不需要重写该方法,但可以重载父类函数实现具体的功能。
第六步,对View的滚动条进行绘制:
可以看到,这里去调用了一下View的onDrawScrollBars()方法,所以我们看下View的onDrawScrollBars(canvas);方法,如下:
/**
* <p>Request the drawing of the horizontal and the vertical scrollbar. The
* scrollbars are painted only if they have been awakened first.</p>
*
* @param canvas the canvas on which to draw the scrollbars
*
* @see #awakenScrollBars(int)
*/
protected final void onDrawScrollBars(Canvas canvas) {
//绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析
......
}
可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。
Draw原理分析
可以看见,绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:
- 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
- View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
- View的绘制是借助onDraw方法传入的Canvas类来进行的。
- 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。
- 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
- 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
到此我们的View绘制流程都已经分析完毕了……………
参考文献:
http://blog.csdn.net/yanbober/article/details/46128379/