在上一篇View绘制流程及源码解析(二)——onMeasure()流程分析这篇文章中,我们详细的分析了很多测量过程的细节(好吧,真是有点过于细节了,感觉寒假的时候这时间花的,简直心疼),这篇我们来分析三大流程最后两个流程,鉴于上一篇文章中事无巨细的分析细节造成的惨痛教训,这篇文章会避免对于细节的死扣,而更加注重整体流程的把握。
一.Layout流程
1.Layout的基本过程
Layout的过程就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有的子元素并调用其Layout方法,在Layout方法中onLayout方法又会被调用。这段话摘自《Android开发艺术探索》,既然Layout的过程是ViewGroup用来确定子元素的位置的,那么ViewGroup的位置又是怎么确定的呢?实际上,类似于Measure()过程,整个布局的过程也是一个递归调用的过程——首先从最顶层的DecorView开始,我们说过,DecorView是一个FrameLayout,也就是一个ViewGroup,调用他的Layout方法,那么他会遍历循环他的子元素,并调用子元素的onLayout方法,如果子元素依然是个ViewGroup,那么调用这个子ViewGroup(如LinearLayout)重写过的onLayout方法;然后在该方法中又会接着往下遍历,调用子View的layout方法,一直扒到最后一层View,假设这个子View是TextView,那么就会调用TextView的onLayout的方法,具体的控件,他们的onLayout的方法都是重写过的,每个有每个自己的布局规矩。
这里我们再贴一下前两篇中都提到的图,大家可以对着这个图心理想一遍这个递归调用的过程。
2.DecorView的Layout过程
首先我们要回到三大流程开始的地方,也就是ViewRootImpl类中的performTraversals()方法中:
(framewoks/base/core/java/android/view/ViewRootImpl):
if (didLayout) {
performLayout(lp, mWidth, mHeight);
在View绘制流程及源码解析(一)——performTraversals()源码分析这篇文章的“第二段代码”前面的一段中,我们说了这个mWidth与mHeight,这两个值实际上就表示的是当前窗口(Window)的大小,或者说当前DecorView的宽高,lp就是DecorView的布局参数。
我们接着看这个方法(framewoks/base/core/java/android/view/ViewRootImpl):
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
final View host = mView;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
这个方法中,可以看到host调用了layout()方法,这个host是mView赋值过来的,这个mView就是DecorView,我们去到View类中看下这个layout()方法(framewoks/base/core/java/android/view/View):
/*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public void layout(int l, int t, int r, int b) {
......
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);
}
}
}
......
}
可以看到,调用了onLayout(changed, l, t, r, b);
方法,这个方法的四个参数,看注释可以知道,依次是View/ViewGroup的左、上、右、下四个坐标,而他得调用方法是host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
,这里的host变量就是DecorView,那么传入的这四个参数是什么意思呢?首先我们知道,DecorView是Android屏幕的的顶级View,他上面已经没有父View了,而我们知道,Android当中的原点坐标系是从左上角开始的:
我们贴一张之前贴过的图,注意左上角的那个红点,我们已经标出了他是Android坐标系的原点,以及DecorView的区域示意图(忽略过扫描区域,那玩意是在屏幕外面的)。因此,我们也不难理解,host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
中的参数了,前两个0,0表示的是Android的坐标系原点(DecorView的左上角),后两个表示是DecorView的测量宽高。
回到上面的源码中,我们可以看到在layout()方法中,调用了onLayout()方法,我们来看看这个方法:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
如你所见,这个方法是空的——上面我们已经提到,onLayout的实现方法,是整个下发到子View/ViewGroup中的,具体的View具体实现,我们对着上面的screen_simple.xml的结构层次图,试着分析一下LinearLayout的onLayout()方法:
3.LinearLayout的Layout过程
(framewroks/base/core/java/android/widget/linearlayout):
@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);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight; //确定子元素的右界限
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight; //子元素的生存空间为,LinearLayout的宽度减左右Padding
final int count = getVirtualChildCount(); //获取子元素的个数
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) { //根据BOTTOM,CENTER_VERTICAL,TOP三种Gravity来确定childTop,即子元素的顶部位置
case Gravity.BOTTOM:
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
......
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) { //可见性不是GONE的元素都参与布局
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
......
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { //根据三种Gravity确定子元素左位置
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
......
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
......
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
上面的流程比较清楚,我们加了一些必要的注释,就不死扣实现的细节了,可以看到调用了setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);
这个方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
可以看到,这个方法中又调用了子View的layout()方法,如果此时子View也是个ViewGroup,那么回到上面的流程中接着调用;如果此时子View是一个View,那么调用子View(如TextView)的onLayout方法。
二.Draw流程
1.Canvas与Surface
上面performLayout()方法之之心完之后,就该执行performDraw()方法了,我们直接来看这个方法(framewoks/base/core/java/android/view/ViewRootImpl):
private void performDraw() {
......
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
......
}
调用了draw(fullRedrawNeeded);
,这个fullRedrawNeeded意思是,需要完全重绘的意思,笔者看了下这个变量,整个ViewRootImpl类中,只有上面一段中的这句:
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mFullRedrawNeeded的值变为了false,其他地方均为true,也就是说,你可以先不管这个boolean值代表什么,只要知道他是一个true值就行了。
然后我们接着看draw(fullRedrawNeeded);
方法:
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return;
}
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
......
1).什么是surface?
这个surface是什么呢?Surface是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。SDK的中对该类的注释为:Handle onto a raw buffer that is being managed by the screen compositor
——处理由屏幕合成器管理的原始缓冲区。
- 句柄,英文:HANDLE,数据对象进入内存之后获取到内存地址,但是所在的内存地址并不是固定的,需要用句柄来存储内容所在的内存地址。从数据类型上来看它只是一个32位(或64位)的无符号整数。
- Surface 充当句柄的角色,用来获取原始图像缓冲区以及其中的内容。
- 原始图像缓冲区(raw buffer)用来保存当前窗口的像素数据。
看了这段话,我们大概已经知道了这个Surface是用来干什么的了,他就是一个用来获取原始图像缓冲区中图像数据的句柄~~我们接着看if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty))
这个巨长的方法:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
try {
......
canvas = mSurface.lockCanvas(dirty);
......
} catch (IllegalArgumentException e) {
......
}
......
try {
......
mView.draw(canvas);
.......
2).什么是Canvas ?
上面这段代码中出现了一个Canvas对象,我们省略了除了关键代码之外的所有代码。首先说一下Canvas是什么?
先看下这个类的注释:
/**
* The Canvas class holds the "draw" calls. To draw something, you need
* 4 basic components: A Bitmap to hold the pixels, a Canvas to host
* the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,
* Path, text, Bitmap), and a paint (to describe the colors and styles for the
* drawing).
* /
为了绘制一个东西,我们需要4个元素来协同完成:
- 位图:Bitmap 来保持(hold)那些像素
- 画布:Canvas 来响应画画(draw)的调用(并将其写入 bitmap)
- 画笔:paint 描述画画的颜色和样式等
- “颜料“:drawing primitive,比如矩形、路径、文字、位图等其他元素
关于这四个元素,我们如果了解自定义View的话应该很熟悉,这个Canvas实际上就是一个画布.
3).Surface与Canvas
可以看到,每一个Surface中保存了一个Canvas对象,因为Canvas是“画布”嘛,它可以暂时保存我们绘画的数据(当然最终是要存到BitMap中);而Surface是操纵图像数据的句柄,因此他保存一个Canvas,通过这个Canvas我们就可以进行各种绘制的工作。
不论是普通的自定义View,还是SurfaceView自定义Veiw,我们都需要首先获取Surface中的Canvas,通过canvas = mSurface.lockCanvas(dirty);
方法,然后才能进行一系列操作,并在操作完成后unlockCanvasAndPost(canvas)
方法释放canvas——只不过在SurfaceView中需要手动调用这两个方法,但是在普通的View或者自定义View中,这两个方法是在ViewRootImpl类中由系统自行调用。
我们看看这句:canvas = mSurface.lockCanvas(dirty);
,这句代码调用了Surface类中的lockCanvas()方法(framewoks/base/core/java/android/view/Surface):
public Canvas lockCanvas(Rect inOutDirty)
throws Surface.OutOfResourcesException, IllegalArgumentException {
synchronized (mLock) {
checkNotReleasedLocked();
if (mLockedObject != 0) {
throw new IllegalArgumentException("Surface was already locked");
}
mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
return mCanvas;
}
}
可以看到,这个方法中,我们返回了一个Canvas对象,并调用native层的方法nativeLockCanvas()锁定这个画布。
2.绘制流程
之后我们就可以看上面那段代码中真正的开始绘制的地方了:mView.draw(canvas);
,我们之前说过,mView表示的就是DecorView:
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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);
// 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);
// we're done...
return;
}
......
}
可以看到,上面的注释中说的很清楚了,绘制的过程主要分为4个步骤(本来6个步骤,一般情况下跳过2和5):
①绘制背景 drawBackground(canvas);
②绘制自己onDraw(canvas)
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
这个方法是一个空方法,不同的子View会重写这个方法来实现自己的绘制;ViewGroup里边没有实现这个方法,笔者看了一下,貌似只有LinearLayout实现了这个方法,但也仅仅是为了在其中调用分割线(drawable)的draw()方法,
③绘制childrendispatchDraw(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类中确实实现的(framewoks/base/core/java/android/view/ViewGroup):
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
......
可以看到,在ViewGroup的这个方法中,我们循环遍历了子View,并调用他们的draw方法,如此draw事件就这样一层层传递下去了。