目录
0.前置
上层容器负责下层子控件的测量和绘制,并传递交互事件。通常Activity界面由顶部的titleView和下方contentView组成,如果activity中要通过RequestWindowFeature(NO_TITLE)来不显示titleView,此步骤需放到setContentView()操作之前。之后,程序在onCreat()的setContentView()之后,会回调onResume()。此时会将整个DecorView添加到PhoneWIndow中,并显示以完成最终绘制。
1.自定义View简介
理解View的基本流程(包括测量流程,布局流程,绘制流程),可以实现各种效果的自定义View。
自定义View可以分成两大类:
1.继承视图ViewGroup (ViewGroup、LinearLayout、FrameLayout、RelativeLayout等)
2.继承控件View(View、TextView、ImageView、Button等)
View的绘制基本上由measure()、layout()、draw()这个三个函数完成,再讲3大流程前,先理解几个名词。
2.MeasureSpec
MeasureSpec参与View的测量工作,自身为32位的int值,高二位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)。SpecMode和SpecSize都为int类型。一组SpecMode和SpecSize可以组合成一个MeasureSpec。也可通过MeasureSpec去拿到SpecMode或SpecSize。
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
public static class MeasureSpec {
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
}
可以看出View的静态内部类MeasureSpec,定义了3常量,UNSPECIFIED和EXACTLY和AT_MOST,这3个都是父容器对子容器的约束等级,同时也能看出getMode和getSize函数能获取到父容器的大小值和约束类型。
正常情况使用View指定的MeasureSpec进行View的测量,同时可给View设置LayoutParams。LayoutParams在View测量时,会在父容器的约束下转换成对应MeasureSpec,从而确定View的宽高位置。
因此,View的MeasureSpec确定(即View的宽高确定)需要父容器的MeasureSpec约束,加上View的LayoutParams才行。下面验证一下。View的测量由measure函数实现,但是,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) {
//父容器施加了一个精确的尺寸
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父容器指定最大值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父容器不限制大小
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
翻译如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
获取限制信息中的尺寸和模式。
switch (限制信息中的模式) {
case 当前容器的父容器,给当前容器设置了一个精确的尺寸:
if (子View申请固定的尺寸) {
你就用你自己申请的尺寸值就行了;
} else if (子View希望和父容器一样大) {
你就用父容器的尺寸值就行了;
} else if (子View希望包裹内容) {
你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;
}
break;
case 当前容器的父容器,给当前容器设置了一个最大尺寸:
if (子View申请固定的尺寸) {
你就用你自己申请的尺寸值就行了;
} else if (子View希望和父容器一样大) {
你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;
} else if (子View希望包裹内容) {
你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;
}
break;
case 当前容器的父容器,对当前容器的尺寸不限制:
if (子View申请固定的尺寸) {
你就用你自己申请的尺寸值就行了;
} else if (子View希望和父容器一样大) {
父容器对子View尺寸不做限制。
} else if (子View希望包裹内容) {
父容器对子View尺寸不做限制。
}
break;
} return 对子View尺寸的限制信息;
}
不难看出,父容器的MeasureSpec和View的LayoutParams确定子View的MeasureSpec,同时View的margin和padding也会有影响。
考虑几种情况:
a.xml指定view为100dp或者match_parent,在不重写measure函数时,view分别显示大小就为100dp和铺满父布局。
b.如果我们又不重写measure函数,且xml中指定物体大小为wrap_content,那么系统就会默认的让view铺满父布局,因此我们重写measure函数目的大多是告知系统让view在wrap_content情况下,给view重新设置一个默认大小。
1) SpecMode
UNSPECIFIED:父容器不对内部view做大小限制
EXACTLY:父容器已检测出view的精确大小,此时view大小即为SpecSize所指定的值。此情况对应为view设置了match_parent或者指定值大小的LayoutParams这两模式
AT_MOST:父容器指定最大值,View的大小不能超出。此情况对应于View设置了wrap_content的LayoutParams。
3.View的工作流程
如果是view,measure函数就可以完成测量过程。如果是viewGroup,除了自身测量外,同时也会去遍历子元素的measure函数。
1) View的measure过程
常量方法measure内部调用onMeasure方法,只需看onMeasure的实现。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
测量完大小之后都必须调用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;
}
可以看出AT_MOST和EXACTLY父布局限制条件下,getDefaultSize()返回的就是MeasureSpec中测量大小过的SpecSize,注意的是这里只是返回测量大小,View真正大小是在layout流程中决定的。但是几乎所有情况下测量过后的大小和最终大小是相等的。而UNSPECIFIED父布局限制条件下的测量大小为getSuggestedMinimumWidth()/getSuggestedMinimumHeight()的返回值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
返回值与View是否设置背景相关。
2)ViewGroup的measure过程
ViewGroup通过measureChild(int widthMeasureSpec, int heightMeasureSpec)遍历测量子元素大小
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(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
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);
}
可以看出measureChild()方法,拿到子元素的LayoutParams,再根据父容器的MeasureSpec和padding值,然后去生成子元素的MeasureSpec,最终交由view的measure处理。
3)layout流程
当ViewGroup位置确定后,会在onLayout方法中遍历子元素的layout()方法去决定子元素位置,看下layout()源码
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,决定子元素在viewgroup中的位置
onLayout(changed, l, t, r, b);
...
}
...
}
layout方法中先通过setFrame()方法确定子View的四个顶点位置,又重新调用onLayout,确定子元素在viewgroup中的位置。
4)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;
/*
* 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, 画背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2和5步骤跳过
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 画内容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, 画子元素view。遍历调用子元素view的draw方法
dispatchDraw(canvas);
// Step 6, 画装饰 (如scrollbars的滚动条)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
return;
}
...
}
可以看出绘制分为6步,其中第二步和第五步通常都是跳过的。我们只看剩下的四步:
- 绘制背景
- 绘制自己的内容(onDraw())
- 绘制子view(dispatchDraw())
- 绘制装饰
细节还是很多的,其中dispatchDraw()会遍历所有子元素的draw方法,使得draw事件一层层的传递。