Android开发学习笔记——自定义View(一)View三大工作流程measure、layout和draw
Android开发学习笔记——自定义View(一)View三大工作流程measure、layout和draw
Android已经为我们提供了众多基础控件,我们可以通过这些控件创建出多种多样的界面布局,但是,在实际开发中,如果界面元素过于复杂有可能无法使用基础控件实现,此时就需要我们根据需求自定义view。在正式开始学习自定义view的方法之前,我们需要对View体系有深入的理解,只有这样才能够写出好的自定义View,同时对View体系的理解能够帮助我们在开发中避免和修改view相关的bug。
View基本介绍
在学习View体系之前,首先,我们需要知道什么是View?View是Android中所有控件的基类,包括ViewGroup,它内部包含多个控件,即view组,比如我们常用的几大布局都属于ViewGroup。ViewGroup也继承自View类,也就是说View本身可以是单个控件也可以是多个控件组成的一组控件。我们可以查看TextView和LinearLayout的继承关系,如下图:
我们可以看到两者继承自View类,其中LinearLayout继承自ViewGroup,而ViewGroup继承自View,对于Android中View的层级关系,具体我们可以查看如下图:
View的三大工作流程
View的工作流程主要是指measure、layout和draw这三大流程,即测量、布局和绘制,View通过measure测量view的宽高,layout确定view的最终宽高和位置,最后通过draw将View绘制到屏幕上。
measure过程
measure过程可以分为两种情况,如果只是一个View,那么通过measure方法即可完成其测量过程,如果是一个ViewGroup,除了完成自己的测量过程为,还需要遍历其子view并完成其measure过程,测量子view的宽高。
view的measure过程
View的measure过程是由其measure方法来完成的,measure方法是一个final类型的方法,子类无法重写,其中会调用View的onMeasure方法,因此,我们只需要看onMeasure方法的实现即可,其源码如下图:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我们可以看到,onMeasure方法实现非常简单,只是调用了setMeasuredDimension方法,其源码如下:
/**
* <p>This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.</p>
*
* @param measuredWidth The measured width of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
* @param measuredHeight The measured height of this view. May be a complex
* bit mask as defined by {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
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);
}
从setMeasuredDimension方法的实现以及相关注释说明中,我们可以推断该方法用于设置view的宽高为measuredWidth和measuredHeight,那么具体其宽高为多少呢?我们需要继续查看getDefaultSize和getSuggestedMinimumWidth方法。其中getDefaultSize方法如下:
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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即测量模式来确定最终的返回结果,我们可以看到当specMode为MeasureSpec.UNSPECIFIED时,其返回结果为方法的第一个参数size,而这个参数是通过getSuggestedMinimumWidth获取到的,其源码如下:
/**
* Returns the suggested minimum width that the view should use. This
* returns the maximum of the view's minimum width
* and the background's minimum width
* ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned width is within the requirements of the parent.
*
* @return The suggested minimum width of the view.
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
通过源码,我们可以看到getSuggestedMinimumWidth方法返回的值为view的最小值,当view设置了背景时返回背景Drawable的原始宽度(如果无原始宽度返回0)与mMinWidth,即android:minWidth的值中的最大值,否则就返回mMinWidth。
然后,我们再回过头来看MeasureSpec的具体实现。
MeasureSpec
MeasureSpec为View的一个内部类,其在很大程度上决定了一个View的尺寸规格,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换为对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。
MeasureSpec代表了一个32位的int值,其中高2位代表SpecMode(测量模式),低30位代表SpecSize(测量规格大小)
对于MeasureSpec,我们首先需要主要了解其中的几个常量以及getMode和getSize方法:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
从上述代码中,我们可以了解到其工作模式,getMode方法获取到measureSpec的高2位为specMode,getSize获取到measureSpec的低30位为specSize。其中specMode包含了UNSPECIFIED、EXACTLY和AT_MOST三种模式,具体说明如下:
模式 | 说明 |
---|---|
UNSPECIFIED | 父容器不对view做任何限制,一般用于系统内部 |
EXACTLY | 父容器已经检测出view所需要的精确大小,这个时候view的最终大小就为SpecSize,对应于LayoutParams中的match_parent和具体数值两种情况 |
AT_MOST | 父容器指定了一个可用大小SpecSize,view的大小不能超过这个值,具体是多少要看View的具体实现,对应于LayoutParams中的wrap_content |
从上述的getDefaultSize源码,我们可以看出,只要我们确定了view的measureSpec,我们就可以通过其specMode以及specSize确定其测量宽高。那么measureSpec是怎么确定的呢?
MeasureSpec和LayoutParams的对应关系
在view测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,也就是说view的MeasureSpec是由父容器的MeasureSpec以及view本身的LayoutParams共同决定的。
既然,view的MeasureSpec是受到父容器的MeasureSpec影响的,而view的measure过程是由ViewGroup传递而来的,我们可以查看ViewGroup中的measureChild源码:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
显然,child的measureSpec是通过getChildMeasureSpec根据parent的measureSpec和其本身的LayoutParams获取到的,再查看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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
通过上述代码,我们可以总结出view的MeasureSpec的创建规则大致如下图所示,其中parentSize为父容器剩余空间大小:
此时,我们再回过头来看getDefaultSize方法,我们可以总结view的measure过程,大致如下:父容器在测量时确定了子view的measureSpec,通过measureSpec可以确定其测量宽高,当view的specMode为UNSPECIFIED时,其宽高为view的minWidth和background宽度的最大值,否则view的宽高为specSize。而从上述表格中,我们可以发现,当我们直接继承view来自定义控件时,我们需要重写onMeasure方法并设置wrap_content时的自身大小,否则布局中使用wrap_content就相当于match_parent,此时其specMode为AT_MOST,而其specSize为parentSize,也就是说此时view的大小为parentSize,我们可以重写onMeasure如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
//参考TextView、ImageView的处理方式
//其他情况下沿用系统测量规则即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup的measure过程
对于ViewGroup而言,除了完成自己的measure过程以外,还需要遍历调用其子view的measure方法。ViewGroup中没有定义onMeasure()方法,但他定义了measureChildren()方法。
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
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);
}
}
}
我们可以看到,在measureChildren中遍历了ViewGroup的子view并对每个状态不为GONE的child调用了measureChild来进行其measure过程。而正如我们之前分析的measureChild方法的主要流程就是,取出子view的LayoutParams再结合父容器的MeasureSpec通过getChildMeasureSpec创建子view的MeasureSpec,然后将MeasureSpec直接传递个View的measure方法,完成子View的measure过程即可。代码如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup并没有定义其onMeasure方法的实现,因为其测量过程针对于不同的子类的不同布局特性实现不同,如LinearLayout和RelativeLayout等,具体可以查看其子类的源码。
layout过程
layout过程的作用是ViewGroup用于确定子元素的位置,当ViewGroup的位置被确定后,它再onLayout中会遍历所有的子元素并调用其layout方法,在子元素的layout方法中onLayout方法又会被调用。
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>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().</p>
*
* <p>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.</p>
*
* @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) {
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
}
layout的四个参数代表了view到父容器的坐标位置,其中 l 和 t 是子控件左边缘和上边缘相对于父类控件左边缘和上边缘的距离,r 和 b是子控件右边缘和下边缘相对于父类控件左边缘和上边缘的距离。
其中关键代码如下:
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
通过setOpticalFrame和setFrame方法来设定View的位置,同时这两个方法最终都会进入setFrame方法,如下:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
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;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
可以看到,setFrame通过更新mLeft、mRight、mTop和mBottom,并对之前和之后的位置进行重绘来达到改变view位置的目的。View在父容器中的位置确定后,接着会调用onLayout方法用于确定子元素的位置,和onMeasure相同,onLayout也是在ViewGroup的子类中实现的,我们可以了解下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);
}
}
layoutVertical和layoutHorizontal实现逻辑基本相同,我们只需要了解其中一个即可,查看layoutVertical源码如下:
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;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
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) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
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);
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;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
我们可以看到,该方法遍历其所有状态不为GONE的子元素,并通过gravity、padding等一系列条件,计算出其相对位置,并调用setChildFrame方法来为子元素指定对应的位置,其中childTop为逐渐增大,这意味着子元素为被放置到底部,这符合LinearLayout的布局规则。查看setChildFrame方法,如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
我们发现这个方法仅仅是调用child的layout方法而已,这样父容器在通过layout完成自身的布局位置确定后,又会通过onLayout方法去调用子元素的layout方法来确定子元素的位置,这样逐一传递下去就最终完成了整个View树的layout过程。
draw过程
draw过程的作用就是将确定好位置和大小的View绘制到屏幕上面,通过draw方法的源码,我们可以很容易知道draw过程主要分为以下几步:
- 绘制背景
- 保存图层信息canvas(可跳过)
- 绘制view内容
- 绘制所有子元素children
- 绘制View的褪色的边缘,类似于阴影效果(可跳过)
- 绘制装饰Decoration(滚动条)
- 如果必要绘制焦点高亮显示
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
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)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
int saveCount;
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
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 (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
//.....省略
我们可以看到draw过程中通过onDraw来绘制view自身,onDraw就是我们在自定义view时最重要的方法,通过onDraw方法,我们可以绘制控件的具体实现,因此每个view的onDraw方法都是不同的。绘制好view后自身后,通过dispatchDraw来绘制其所有子元素,该方法在View中没有实现,在ViewGroup被实现,在该方法中遍历所有子元素并调用了drawchild方法,如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
我们可以看到调用了子view的draw方法,如此一来即可逐层传递绘制所有的view。