在博客带着问题学习Android中View的measure测量中,我们学习了view的measure测量的原理,以及解决了我们实际项目中遇到的一些问题的解决方案。在这篇博客中我们同样根据问题去学习下layout和draw的源码。
引导问题
在自定义view中为什么onMeasure()方法会调用多次?目的是什么?
在自定义view中draw()的绘制步骤是什么?
layout方法源码分析
概述
在自定义view/viewGroup中,我们一般需要重写onLayout方法进行布局,在这个方法中我们可以根据我们的需求对view的位置或者viewgroup中的子view的位置进行设定。我们在view的源码中查看下:
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
我们看到这个protected方法是一个空方法,一行代码也没有,这就需要我们进行重写该方法,然后实现我们的布局效果。那么这个方法在哪里执行的?就需要我们查找看看。
layout源码分析
经过查找,我们发现在layout()方法中调用了我们重写的onLayout()方法,然后实现我们自定义的布局效果。我们看下源码:
/** * Assign a size and position to a view and all of its * descendants * *
This is the second phase of the layout mechanism. * (The first is measuring). In this phase, each parent calls * layout on all of its children to position them. * This is typically done using the child measurements * that were stored in the measure pass().
* *Derived classes should not override this method. * Derived classes with children should override * onLayout. In that method, they should * call layout on each of their children.
* *@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 */@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
//判断layout之前是否需要进行measure,这里使用位运算符,
//PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT是一个常量,用于标志在layout()之前
//是否需要进行measure(),所以有时onMeasure方法调用多次
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;
//判断view的父view的layoutMode属性是否为LAYOUT_MODE_OPTICAL_BOUNDS,
//然后调用set***Frame()方法进行控件的位置大小信息,以此判断控件大小是否改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果view的大小发生改变或者我们主动要求布局的时候,开始进行重新布局,调用我们
//重写的onLayout方法
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//改变标志位
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//判断view中的layoutChange改变的监听事件是否为null
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)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;
}
我们看下源码中的针对layout方法的解释:“为一个view的大小和位置进行赋值或者它的子项”。layout是布局机制的第二阶段,第一阶段是measure。在这个阶段parent调用layout方法放置他们的子view。view的派生类不能重写layout方法,派生类需要重写onLayout方法,在onL方法中调用layout方法来设置子view的的位置。关于参数:l、t、r、b这四个参数是相对于parent view的位置。
上面代码的注释也还比较清晰,我们主要讲解下changed变量,该变量标识着我们的view大小是否改变。我们看下系统是怎么判断的。这就涉及到这段代码:
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
代码中涉及到两个方法setOpticalFrame和setFrame,我们先看下setFrame方法。
/** * Assign a size and position to this view. * * This is called from layout. * *@param left Left position, relative to parent *@param top Top position, relative to parent *@param right Right position, relative to parent *@param bottom Bottom position, relative to parent *@return true if the new size and position are different than the * previous ones * {@hide} */
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
//判断view的位置大小是否改变,通过比较现在的值和以前存储的值进行比较
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//view大小改变,改变标识的值为true
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
//计算view的旧长宽和本次的长宽值
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//判断view的长宽大小 是否改变
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position,刷新视图
invalidate(sizeChanged);
//存储本次的left、top、right、bottom值为旧值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
//设置l、t、b、r的值
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
//如果view大小改变,调用sizeChange方法
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
// If we are visible, force the DRAWN bit to on so that
// this invalidate will go through (at least to our parent).
// This is because someone may have invalidated this view
// before this call to setFrame came in, thereby clearing
// the DRAWN bit.
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// parent display list may need to be recreated based on a change in the bounds
// of any child
invalidateParentCaches();
}
// Reset drawn bit to original value (invalidate turns it off)
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
实现的原理就是根据本次的l、r、t、b值与上次的值进行比较,判断view的位置大小是否改变,如果view的size改变,调用sizeChange()方法。我们看看这个方法里的东东。
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
if (mOverlay != null) {
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}
rebuildOutline();
}
在这个方法里调用了onSizeChanged()方法,这个方法就是我们自定义view时候,肯能需要重写的onSizeChanged()方法,用于处理我们对view大小改变的处理。这里涉及到一个知识点ViewOverlay这个类,可能很多人不了解,可以看下这个教程,ViewOverlay它是view的最上面的一个透明的层,我们可以在这个层之上添加内容而不会影响到整个布局结构。这个层和我们的界面大小相同,可以理解成一个浮动在界面表面的二维空间。sizeChange方法中判断当前view的ViewOverlay对象是否为null,如果不为null,就进行设置view相对parentView的right值和bottom值。具体涉及可以去看下setRight和setBottom的源码,当然还有setTop和setLeft两个方法。我们就仅仅看下setRight的源码,其它几个类似。
/** * Sets the right position of this view relative to its parent. This method is meant to be called * by the layout system and should not generally be called otherwise, because the property * may be changed at any time by the layout. * *@param right The right of this view, in pixels. */
public final void setRight(int right) {
if (right != mRight) {//判断相对父view右边位置是否变化
//判断transform matrix是否是identity matrix.
final boolean matrixIsIdentity = hasIdentityMatrix();
if (matrixIsIdentity) {
//判断依附的父window是否为null
if (mAttachInfo != null) {
int maxRight;
//记录下新旧最大的right值
if (right < mRight) {
maxRight = mRight;
} else {
maxRight = right;
}
//刷新计算出的新区域
invalidate(0, 0, maxRight - mLeft, mBottom - mTop);
}
} else {
// Double-invalidation is necessary to capture view's old and new areas
invalidate(true);
}
//计算旧的宽度和高度值
int oldWidth = mRight - mLeft;
int height = mBottom - mTop;
mRight = right;
mRenderNode.setRight(mRight);
sizeChange(mRight - mLeft, height, oldWidth, height);
if (!matrixIsIdentity) {//如果不是identity matrix.强制进行刷新一次,根据sizeChange改变后的值
mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
invalidate(true);
}
mBackgroundSizeChanged = true;
invalidateParentIfNeeded();
if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) {
// View was rejected last time it was drawn by its parent; this may have changed
invalidateParentIfNeeded();
}
}
}
draw源码分析
概述
通过上面的分析,我们已经完成了自定义view中的measure和layout两大过程,下面就开始了draw的过程,用于我们绘制我们的事务。在自定义view中,我们需要重写onDraw方法进行绘制:
/** * Implement this to do your drawing. * *@param canvas the canvas on which the background will be drawn */
protected void onDraw(Canvas canvas) {
}
又是一个空方法,“实现此方法来实现我们的绘制”。在View中通过draw方法来调用我们实现的onD方法来实现绘制。接下来我们就要分析draw方法中的绘制步骤?以及何时调用我们的onD方法进行绘制。
/** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is * called. When implementing a view, implement * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
//标识该view的背景是否不透明
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;
//判断view是否设置水平边界渐变和垂直方向边界渐变,fadingEdge属性用来设置拉滚动条时边框渐变的放向。
//none(边框颜色不变),horizontal(水平方向颜色变淡),vertical(垂直方向颜色变淡)。
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,分发draw函数,绘制child view
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars),绘制scrollbars
onDrawScrollBars(canvas);
//如果ViewOverlay不为空,进行分发绘制
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(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;
//判断是否需要paddingOffset
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
//计算left、right、top、bottom的位置
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) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
}
通过代码可以看到draw的绘制顺序:
绘制backgroud
如果有可能为了fading进行保存cavas的图层。
绘制view的内容,此时调用我们重写的onDraw方法
绘制child view
如果有可能,绘制fading edges同时restore Layout图层。
Draw decorations
我们针对draw()的源码进行了简单的注释,在view的绘制过程中,重要的两个方法就是onDraw()和dispatchDraw()两个方法。onDraw()方法是我们自定义view中自己根据需求进行编写函数。dispatchDraw()方法是我们进行绘制子view的方法。View.java中dispatchDraw()默认为空实现,因为其不包含子视图,而ViewGroup重载了dispatchDraw()来对其子视图进行绘制,通常应用程序不应该对dispatchDraw()进行重载,其默认实现体现了View系统绘制的流程。那么,接下来我们继续分析下ViewGroup中dispatchDraw()的具体流程:
@Override
protected void dispatchDraw(Canvas canvas) {
...
if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
} else {
for (int i = 0; i < count; i++) {
final View child = children[getChildDrawingOrder(count, i)];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
......
}
dispatchDraw()的核心代码就是通过for循环调用drawChild()对ViewGroup的每个子视图进行绘制,上述代码中如果FLAG_USE_CHILD_DRAWING_ORDER为true,则子视图的绘制顺序通过getChildDrawingOrder来决定,默认的绘制顺序即是子视图加入ViewGroup的顺序,而我们可以重载getChildDrawingOrder函数来更改默认的绘制顺序,这会影响到子视图之间的重叠关系。
drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制,如果子视图的包含SKIP_DRAW标识,那么仅调用dispatchDraw(),即跳过子视图本身的绘制,但要绘制视图可能包含的字视图。
至此,基本完成了核心代码的分析。继续学习,感觉分析的不透彻,功力还不行啊!