目的
介绍
View
的工作原理,为了更好地自定义View
(这才是学习的重点),需要掌握View
的底层工作原理
(一)初识 ViewRoot 和 DecorView
在正式学习
View
的三大流程之前,我们先了解一下ViewRoot
和DecorView
的概念
(1)ViewRoot
ViewRoot
对应于 ViewRootImpl
类,它是连接 WindowManager
和 DecorView
的纽带,View
的三大流程(测量(measure
),布局(layout
),绘制(draw
))均通过ViewRoot
来完成。View
的绘制流程是从 ViewRoot
的 PerformTraversals
方法开始的,这个方法会依次调用 performMeasure
、performLayout
和 performDraw
三个方法,这个三个方法分别完成顶级 View
的 measure
、layout
和 draw
这三大流程,如此反复就完成了整个 View
树的遍历
(2)DecorView
- Activity
Activity
并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window
。一个Activity
包含了一个Window
,Window
才是真正代表一个窗口。Activity
就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window
、以及View
进行交互。
- Window
Window
是视图的承载器,内部持有一个DecorView
,而这个DecorView
才是view
的根布局。Window
是一个抽象类,实际在Activity
中持有的是其子类PhoneWindow
。PhoneWindow
中有个内部类DecorView
,通过创建DecorView
来加载Activity
中设置的布局R.layout.activity_main
。Window
通过WindowManager
将DecorView
加载其中,并将DecorView
交给ViewRoot
,进行视图绘制以及其他交互。
- DecorView
作为顶级
View
,一般情况下它内部会包含一个竖直方向的LinearLayout
,在这个LinearLayout
里面有上下两部分,上面是标题栏,下面是内容栏。在Acitivity
中我们通过setContentView
所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id
是content
。那么如何得到我们设置的View
呢?代码如下:
ViewGroup content = findViewById(android.R.id.content);
View view = content.getChildAt(0);
DecorView
其实是一个 FrameLayout
,View
层的事件都先经过 DecorView
,然后才传递给我们的 View
(三)理解 MeasureSpec
MeasureSpec
由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。
其中,Mode模式共分为三类
-
UNSPECIFIED
:不对View进行任何限制,要多大给多大,一般用于系统内部 -
EXACTLY
:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值, -
AT_MOST
:对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
(二)View 的工作流程
砰砰砰,敲黑板了!
View
的工作流程主要是指measure
、layout
、draw
这三大流程,即测量、布局和绘制,其中measure
确定View
的测量宽/高,layout
确定View
的最终宽/高和四个顶点的位置,而draw
则将View
绘制到屏幕上
(1)measure 过程
measure
过程,如果只是一个View
,那么通过measure
方法就完成了测量过程,如果是一个ViewGroup
,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure
方法,各个子元素再递归去执行这个流程
1. View 的 measure 过程
首先来看一下 View
的 onMeasure
方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
接着查看 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);
}
这很显然是用来设置 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;
}
SpecMode
是 View
的测量模式,而 SpecSize
是 View
的测量大小,上一节我们了解了 MeasureSpec
。这里很显然根据不同的 SpecMode
值来返回不同的 result
值,也就是 SpecSize
。在 AT_MOST
和 EXACTLY
模式下,都返回 SpecSize
这个值,即 View
在这两种模式下的测量宽和高直接取决于 SpecSize
。也就是说,对于一个直接继承自 View
的自定义 View
来说,它的 wrap_content
和 match_parent
属性的效果是一样的。因此如果要实现自定义 View
的 wrap_content
,则要重写 onMeasure
方法,并对自定义 View
的 warp_content
属性进行处理。而在 UNSPECIFIED
模式下返回的是 getDefaultSize
方法的第一个参数 size
的值,size
的值从 onMeasure
方法来看是 getSuggestedMinimumWidth
方法或者 getSuggestedMinimumHeight
方法得到的。
我们来查看 getSuggestedMinimumWidth
方法做了什么:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果 View
没有设置背景,则取值为 mMinWidth
,mMinWidth
是可以设置的,它对应于 Android:minWidth
这个属性设置的值或者 View
的 setMinimumWidth
的值;如果不指定的话,则默认为 0。setMinimumWidth
方法如下所示:
public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}
如果 View
设置了背景,则取值为 max(mMinWidth, mBackground.getMinimumWidth())
,也就是取 mMinWidth
和 mBackground.getMinimumWidth()
之间的最大值
下面看看 mBackground.getMinimumWidth()
,这个 mBackground
是 Drawable
类的 getMinimumWidth()
方法如下所示:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
intrinsicWidth
得到的是这个 Drawable
的固有宽度,如果固有宽度大于 0 则返回固有宽度,否则返回 0
总结一下:getSuggestedMinimumWidth
方法就是:如果 View
没有设置背景,则返回 mMinWidth
;如果设置了背景,就返回 mMinWidth
和 Drawable
的最小宽度之间的最大值
2. ViewGroup 的 measure 流程
ViewGroup
中没有定义 onMeasure()
方法,但却定义了 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);
}
}
}
遍历子元素并调用 measureChild
方法,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.getLayoutParams()
方法来获得子元素的 LayoutParams
属性,并获取到子元素的 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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
很显然这是根据父容器的 MeasureSpec
的模式再结合子元素的 LayoutParams
属性来得出子元素的 MeasureSpec
属性,有一点需要注意的是如果父容器的 MeasureSpec
属性为 AT_MOST
,子元素的 LayoutParams
属性为 WRAP_CONTENT
,那根据代码我们会发现子元素的 MeasureSpec
属性也为 AT_MOST
,它的 specSize
值为父容器的 specSize
减去 padding
的值,也就是说跟这个子元素设置 LayoutParams
属性为 MATCH_PARENT
效果是一样的,为了解决这个问题需要在 LayoutParams
属性为 WRAP_CONTENT
时指定一下默认的宽和高。
3. LinearLayout 的 measure 流程
ViewGroup
并没有提供 onMeasure()
方法,而是让其子类来各自实现测量的方法,究其原因就是 ViewGroup
有不同的布局的需要很难统一,接下来我们来简单分析一下 ViewGroup
的子类 LinearLayout
的 measure
流程,先来看看它的 onMeasure()
方法 (LinearLayout.java)
:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
来看看垂直 measureVertical()
方法的部分源码:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
mTotalLength = 0;
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
定义了 mTotalLength
用来存储 LinearLayout
在垂直方向的高度,然后遍历子元素,根据子元素的 MeasureSpec
模式分别计算每个子元素的高度,如果是 wrap_content
则将每个子元素的高度和 margin
垂直高度等值相加并赋值给 mTotalLength
得出整个 LinearLayout
的高度。如果布局高度设置为 match_parent
者具体数值则和 View
的测量方法一样
(2)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 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;
}
传进来里面的四个参数分别是 View
的四个点的坐标,它的坐标不是相对屏幕的原点,而且相对于它的父布局来说的。 l
和 t
是子控件左边缘和上边缘相对于父类控件左边缘和上边缘的距离;r
和 b
是子控件右边缘和下边缘相对于父类控件左边缘和上边缘的距离。来看看 setFrame()
方法里写了什么:
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 + ")");
}
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);
...省略
}
return changed;
}
在 setFrame()
方法里主要是用来设置 View
的四个顶点的值,也就是 mLeft
、mTop
、mRight
和 mBottom
的值。在调用 setFrame()
方法后,调用 onLayout()
方法:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
onLayout()
方法没有去做什么,这个和 onMeasure()
方法类似,确定位置时根据不同的控件有不同的实现,所以在 View
和 ViewGroup
中均没有实现 onLayout()
方法。既然这样,我们就来看看 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
做了什么呢?
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);
}
}
}
这个方法会遍历子元素并调用 setChildFrame()
方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
在 setChildFrame()
方法中调用子元素的 layout()
方法来确定自己的位置。我们看到 childTop
这个值是逐渐增大的,这是为了在垂直方向,子元素是一个接一个排列的而不是重叠的。
(3)View的draw流程
View
的 draw
流程很简单,先来看看 View
的 draw()
方法:
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;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
...
// 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;
...
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
}
从源码的注释我们看到 draw
流程有六个步骤,其中第2步和第5步可以跳过:
- 如果有设置背景,则绘制背景
- 保存
canvas
层 - 绘制自身内容
- 如果有子元素则绘制子元素
- 绘制效果
- 绘制装饰品
(scrollbars)