前言:
很多时候,我们在看源码时,看的时候可以理解其原理,但是过后不久又容易忘记,这是因为没有留下一个印象,知道自己看了啥,但是又感觉说不出来,这就是没有归纳总结导致的原因;
那么当前已经有很多人写了View的绘制流程,这里会通过图文的方式来进行总结!希望对你有所帮助;
视觉效果
在开始之前,我们先来看一张图片:
很熟悉的淘宝首页,而Android的大部分界面都是图形界面,这些图形到底是怎么来的呢?系统是怎么绘制出来的呢?让我们带着思考继续看下去;
View的层级关系
1,首先,先来看一下View的层级关系,View的最顶层是Activity,Activity里面是PhoneWindow,而PhoneWindow里面则是最顶层的View(DecorView),DecorView里面包含的就是我们肉眼可以看到的图形界面了,也就是上面的淘宝首页,而绘制的起点也正是从DecorView开始的;
从上面的图可以清楚的看出各个层级的关系,到了DecorView这一层,就是我们最熟悉的View树结构了;
那么到了这里,又会有一个疑问了,DrcorView里面是怎么将View树绘制出来的呢?
别急,且听我细细道来;
下面我们先来看一张图:
这张图详细表明了DecorView添加到Window的过程,这里面看到了一个很熟悉的方法,requestLayout(),这个方式是在ViewRootIml里面调用的,来看看官方API的解释:
Called when something has changed which has invalidated the layout of a child of this view parent.
这句话什么意思呢? 翻译过来的意思就是调用这个方法会导致当前视图的子View布局失效,也就是说调用这个方法会导致View树的重新layout;
ViewRootIml
在DecorView调用了requestLayout()方法之后,最终会走到View的绘制流程,前面流程的源码我这里就不贴出来了,建议看完自己跟着源码走一遍;
最终的绘制流程是在performTraversals()方法里面;
private void performTraversals() {
// Ask host how big it wants to be, 执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//执行布局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
//执行绘制操作
performDraw();
}
performTraversals()里面的源码很长很复杂,这里我们只需要关注测量,布局,绘制这几个方法即可;
看一下视图绘制的流程图:
让我们回忆一下之前的问题,答案现在已经很明显了,就是系统调用ViewRootIml类里的performTraversals()去绘制整个界面的;
什么?这就没了???
这位大侠,请放下你手中的刀,我还没讲完呢!
前面已经把View绘制流程的入口已经理清楚了,那么接下来就继续分析performTraversals()里的调用吧;
测量
先来看一下performMeasure()方法的源码:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这一段源码很简单,就是调用了View的mesure方法,这个mView就是最顶层的DecorView,这里传了(childWidthMeasureSpec, childHeightMeasureSpec)这两个参数,那么这两个参数是什么意思呢?有什么用呢?请继续往下看;
测量规格
在调用performMeasure()方法之前,会先调用getRootMeasureSpec()方法来获取测量规格,也就是childWidthMeasureSpec, childHeightMeasureSpec这两个参数;
看一下getRootMeasureSpec()方法的源码:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window cant 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;
}
在继续深入分析之前,先来看一下测量模式的类型:
1.<font color=#ff0000 face="黑体">MeasureSpec.EXACTLY</font>:确定模式,父容器希望子视图View的大小是固定,也就是specSize大小。这里可以理解为有具体的大小,比如MATCH_PARENT或者10dp这种;
2.<font color=#ff0000 face="黑体">MeasureSpec.AT_MOST</font>:最大模式,父容器希望子视图View的大小不超过父容器希望的大小,也就是不超过specSize大小。这里理解为没有固定的大小,由子类去计算,对应WRAP_CONTENT这种;
3.<font color=#ff0000 face="黑体">MeasureSpec.UNSPECIFIED</font>: 不确定模式,子视图View请求多大就是多大,父容器不限制其大小范围,也就是size大小。这种可以理解为没有对子View添加束缚,比如列表控件,RecyclerView,ListView这种;
接下来再回到getRootMeasureSpec()这个方法中,源码根据传进来的宽高来获取测量的规格;
第一个case为ViewGroup.LayoutParams.MATCH_PARENT时:
使用了MeasureSpec.EXACTLY的测量模式,也就是有具体的大小;
第二个case为ViewGroup.LayoutParams.WRAP_CONTENT时:
使用了MeasureSpec.AT_MOST的测量模式,也就是没有具体的大小;
第三个case为MeasureSpec.UNSPECIFIED:
和第一个相同;
DecorView默认的宽高为MATCH_PARENT,那么这里就会走第一个case去获取测量规格,也就是说最顶层的测量规格就是从这里获取的;
View的测量
到这里,测量规格弄清楚了,接下来分析View的measure()方法;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 判断是否需要强制布局,也就是会触发重新测量
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
// 将当前的规格和上一次测量的规格做比较,判断是否需要重新测量
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
这里主要判断是否需要重新测量,如果需要则调用onMeasure()去测量;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure()方法很简单就几行代码,如果子类没有重写这个方法去测量宽高,则使用默认的方法getDefaultSize()去获取宽高,然后再调用setMeasuredDimension()去设置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;
}
通过宽高size和测量的规格measureSpec来计算最终的宽高,也就是说如果布局里面的子View没有重新onMeasure()时,则会使用默认的方法来获取宽高,那么布局使用测量模式为MeasureSpec.EXACTLY和MeasureSpec.AT_MOST时,宽高都是返回由测量模式和具体大小计算之后的值specSize;
那么到这里测量的方法差不多就分析完了,但是还有一个疑问,也就是View树是怎么测量的呢?
接下来继续分析;
前面分析的是子View没有重新onMeasure()的情况,接下来分析子View重写了onMeasure()的情况;
LinearLayout的测量
举个熟悉的例子,LinearLayout控件是我们最常用的ViewGroup控件,下面以这个为例子来进行分析;
看一下LinearLayout控件的onMeasure()方法:
rotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
再看一下measureVertical的方法:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);\
...
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
}
...
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
}
这源码里面通过遍历当前的子View,然后通过measureChildBeforeLayout()去测量子View的宽高,并通过计算子View的宽高来调用setMeasuredDimension()设置LinearLayout的宽高,而测量子View 的方法里面最终调用的是ViewGroup的measureChildWithMargins()方法;
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
ViewGroup里的方法最终调用了View里的measure方法,而ViewGroup里面也自定义了获取测量模式的方法getChildMeasureSpec(); 这里细节就不过多关注了;
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);
}
那么到这里,ViewRootIml的performMeasure()方法的流程就可以总结为下面这张图:
整个View树的测量流程就是通过这种递归的方式,一步步的测量完成的;
总结:
1,View树的测量是通过递归的方式测量完成的,递归的方法为View的measure()方法;
2,View和ViewGroup都有自己的测量模式的方法,当然子View也可以自定义获取测量模式的方法;
3,View树测量结束之后,会调用setMeasuredDimension()让之前测量的宽高设置生效,这个方法是在递归结束之后,通过View树的最底层往上传递的;
4,子View的大小是由父视图和子视图共同决定的;
测量的流程已经讲完了,接下来开始讲布局的流程,既然测量的流程是通过递归的方式,那么布局的流程是不是也?
是的,没错,也是通过递归的方式;
别急,且请我细细道来!
布局
对View进行布局的目的是计算出View的尺寸以及在其父控件中的位置,具体来说就是计算出View的四条边界分别到其父控件左边界、上边界的距离,即计算View的left、top、right、bottom的值。
先来看一下performLayout()方法的源码:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
// 标记布局开始
mInLayout = true;
final View host = mView;
...
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
// 标记布局结束
mInLayout = false;
}
这里调用了mView的layout()方法,这个mView就是最顶层的DecorView,而layout()方法则为View里的方法;
View的布局
看看View的layout()方法里面做了啥?
public void layout(int l, int t, int r, int b) {
...
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法,
//否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(),
//所以无论如何都会执行setFrame()方法。
//setFrame()方法会将View新的left、top、right、bottom存储到View的成员变量中
//并且返回一个boolean值,如果返回true表示View的位置或尺寸发生了变化,
//否则表示未发生变化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//如果View的布局发生了变化,或者mPrivateFlags有需要LAYOUT的标签PFLAG_LAYOUT_REQUIRED,
//那么就会执行以下代码
//首先会触发onLayout方法的执行,View中默认的onLayout方法是个空方法
//不过继承自ViewGroup的类都需要实现onLayout方法,从而在onLayout方法中依次循环子View,
//并调用子View的layout方法
onLayout(changed, l, t, r, b);
...
}
...
}
这里只需要关注setFrame()方法和onLayout()方法即可,onLayout()方法由子类实现,先来看一下setFrame()方法;
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//将新旧left、right、top、bottom进行对比,只要不完全相对就说明View的布局发生了变化,
//则将changed变量设置为true
changed = true;
...
// 分别计算View的新旧尺寸
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
// 比较View的新旧尺寸是否相同,如果尺寸发生了变化,那么sizeChanged的值为true
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
...
// 将新的left、top、right、bottom存储到View的成员变量中
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
//如果View的尺寸和之前相比发生了变化,那么就执行sizeChange()方法,
//该方法中又会调用onSizeChanged()方法,并将View的新旧尺寸传递进去
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
}
return changed;
}
在该方法中,会将新旧left、right、top、bottom进行对比,只要不完全相同就说明View的布局发生了变化,则将changed变量设置为true。然后比较View的新旧尺寸是否相同,如果尺寸发生了变化,并将其保存到变量sizeChanged中。如果尺寸发生了变化,那么sizeChanged的值为true。
然后将新的left、top、right、bottom存储到View的成员变量中保存下来。并执行mRenderNode.setLeftTopRightBottom()方法会,其会调用RenderNode中原生方法的nSetLeftTopRightBottom()方法,该方法会根据left、top、right、bottom更新用于渲染的显示列表。
而onLayout()方法由子类实现,如果子类是View的话,则方法不需要实现,如果是ViewGroup的话,因为方法为抽象方法,那么必须由子类实现;这里通过LinearLayout的onLayout()方法来进行举例说明;
LinearLayout的布局
看一下LinearLayout的onLayout()方法:
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();
void layoutVertical(int left, int top, int right, int bottom) {
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
...
}
}
}
通过遍历所有的子View,调用setChildFrame()进行布局,再看一下setChildFrame()方法的源码;
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
这里调用了View的layout()的方法来进行子View的layout;
到这里,布局的流程就分析完了,看一下流程图:
总结:
1,View树的布局是通过递归的方式测量完成的,递归的方法为View的layout()方法;
2,View和ViewGroup都有onLayout()方法,但是ViewGroup的方法是抽象的,必须由子类实现,View的布局是由ViewGroup来控制的,也就是说View并不需要进行onLayout();
3,使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值;
那么到这里,布局的流程就已经讲完了,接下来分析绘制的流程;
既然测量,和布局都是用递归的方式,那绘制岂不是也?
是的,继续往下看,理解了一个之后,其他理解起来也不难!
绘制
最后一步的绘制会将页面展示在我们面前,前面的操作都只是准备工作;
先来看一下performDraw()的源码:
private void performDraw() {
...
try {
boolean canUseAsync = draw(fullRedrawNeeded);
...
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这里调用了ViewRootIml里面的draw()方法,跟踪源码发现最终调用的是drawSoftware()方法;
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
try {
// 从surface里面获取canvas对象
canvas = mSurface.lockCanvas(dirty);
...
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
...
return false;
} finally {
dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value.
}
try {
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
...
try {
...
// 调用View的draw()方法
mView.draw(canvas);
} finally {
...
}
} finally {
...
}
return true;
}
drawSoftware()方法里面先从mSurface获取canvas对象,然后通过mView调用draw()方法时,将canvas作为参数传进去;最后调用的是View的draw()方法;
View的绘制
接下来分析一下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
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
if (drawTop) {
canvas.saveUnclippedLayer(left, top, right, top + length);
}
if (drawBottom) {
canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
}
if (drawLeft) {
canvas.saveUnclippedLayer(left, top, left + length, bottom);
}
if (drawRight) {
canvas.saveUnclippedLayer(right - length, top, right, bottom);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// 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
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
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);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
从上面的源码分析得知,View的draw()方法总共分为6步:
1,绘制视图的背景;
2,如果需要,保存画布的图层以备渐变用;
3,绘制当前视图的内容;
4,绘制子View的视图;
5,如果需要,绘制视图的渐变效果并恢复画布;
6,绘制装饰(比如滚动条scrollbars);
总结为流程图如下:
这里需要关注的是第二步和第四步,第二步是通过调用onDraw()方法绘制当前视图的内容,第四步是调用dispatchDraw()来绘制子View的视图;
先来看一下View的onDraw()方法:
protected void onDraw(Canvas canvas) {}
是一个空方法,交由子类去实现;如果实现来自定义View,那么就得重新该方法去实现绘制的逻辑;
再看一下dispatchDraw()方法:
protected void dispatchDraw(Canvas canvas) {}
这个方法也是个空方法,也是要交由子类去实现;
查看源码的实现有很多个,这里我们只关注ViewGroup的实现逻辑;
ViewGroup的绘制
看一下ViewGroup的实现逻辑:
protected void dispatchDraw(Canvas canvas) {
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
// 遍历子View设置动画效果
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
}
}
...
}
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 遍历子View进行绘制
more |= drawChild(canvas, transientChild, drawingTime);
}
...
}
...
}
...
}
源码里面通过遍历所有的子View,调用drawChild()来进行绘制,继续跟进drawChild()方法里面;
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
这里又看到了熟悉的View,调用了View的draw()方法绘制子View;
到这里,performDraw()方法的流程就讲完了,看一下总结流程图:
总结:
1,View树的绘制流程也是通过递归的方式来进行绘制的,递归的方法为View的draw()方法;
2,ViewGroup和View都可以重新onDraw()方法来实现绘制的逻辑,子View不需要重写dispatchDraw()方法;
3,绘制视图的背景,渐变效果和装饰都是在View的draw()方法里面调用的;
最后总结:
performTraversals()方法的流程分析完毕了,现在终于知道了View的绘制流程为什么分为onMeasure(),onLayout(),onDraw()这三个步骤了;贴出来的源码省略了很多细节,主要是为了把绘制的流程理清,建议可以自己跟着源码去走一遍;
参考:
1,https://www.jianshu.com/p/58d22426e79e
2,https://blog.csdn.net/feiduclear_up/article/details/46772477
3,https://blog.csdn.net/luoshengyang/article/details/8303098
4,https://www.jianshu.com/p/4a68f9dc8f7c
5,https://www.2cto.com/kf/201512/454595.html
关于我
如果我的文章对你有帮助的话,请给我点个❤️,也可以关注一下我的Github和博客;
欢迎和我沟通交流技术;