View在Android中可谓是无处不在,我们看到的每个界面都是View绘制出来的,系统给我们提供的各种控件,比如TextView,Button等他们的本质都是一个个的View,但是这些控件可能并不能满足日常开发的需求,这时候就需要自定义view去绘制一些特定的界面。因此了解View的绘制流程就显得更加重要。
1.ViewRoot和DecorView
我们首先了解一下ViewRoot和DecorView,ViewRoot对应的是ViewRootImp,我们启动一个Activity所展示出来的界面是由window管理的。Window在Activity中对应着PhoneWindow这个实现类。DecorView则是每个Activity的根视图,Activity在被创建后就会被添加到window中去。同时创建ViewRootImpl,并将ViewRootImpl与DecorView建立关系。DecorView本质上是个frameLayout,里面会包含一个LinearLayout有个TitleBar,下面是一个id为content的内容栏。这个样式会根据你设置的主题内容不同而发生改变,但一定会有一个id为content的内容栏。
网上有个图展示的很清楚,借来学习一下图片来自https://www.jianshu.com/p/c151efe22d0d
2.绘制流程
View的绘制从ViewRoot的performTraversals方法开始,经历measure,layout和draw最终将一个view 绘制出来。performTraversals的方法很长,但是里面主要的就是performMeasure,performLayout,performDraw这三个核心代码如下
private void performTraversals(){
...
// 测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
// 布局
performLayout(lp, mWidth, mHeight);
...
// 绘制
performDraw()
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//调用view的measure
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
//掉用view的layout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
.......
}
private void performDraw() {
boolean canUseAsync = draw(fullRedrawNeeded);
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
MeasureSpec
分析三大流程之前我们先了解下MeasureSpec这个类,MeasureSpec是View的一个静态内部类。MeasureSpec字面上理解是测量的规格。MeasureSpec是一个32位的int值,高两位代表测量模式,低30位表示测量规格大小。将模式和大小打包成一个int值目的是避免过多的内存分配。
我们看到为了方便使用,MeasureSpec提供了makeMeasureSpec,makeSafeMeasureSpec打包方法,和getMode,getSize解包的方法。
我们来分析一下SpecMode的三种类型,源码注释如下
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
* 父容器不对view大小做限制,view的可以想多大就多大
*/
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.
* 父容器检测出view所需的精确大小,对于view来说大的大小就是SpecSize的大小。
* 对应LayoutParams中的mach_parent和具体数值
*/
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.
* 父容器指定一个大小,view最大不能超过大于这个值
* *对应LayoutParams中的warp_content这个模式
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
MeasureSpec与LayoutParams
系统通过MeasureSpec进行测量,但是我们也可以通过给view设置LayoutParams。在view测量的时候系统会将layoutParams在父布局的约束下转换成对应的MeasureSpec。因此决定MeasureSpec的值需要LayoutParams和父容器一起,从而决定view的宽高。
-
对于顶级的DecorView它的MeasureSpec是由窗口尺寸和自身的LayoutParams决定的。
-
普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
具体见ViewGroup中的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);
}
《Android开发艺术探索》中关于View的MeasureSpec的创建规则总结了一个表格。这里copy来学习一下
小结一下
1.当View固定大小的时候,无论父布局是什么模式,View的MeasureSpec都是精准模式。大小按照LayoutParam中指定的来。
2.当view是match_parent时,父布局是精准模式view就是精准模式,父布局是最大模式,view就是最大模式。尺寸大小最大不能超过父布局。
3.当view是warp_content时,无论父布局是精准还是最大,view总是最大模式。
一, measure过程
measure分为View和ViewGroup两种情况。ViewGroup除了测量完自己的宽高还会还要去遍历调用所有子元素的measure方法。
(1)View的measure
view的measure会调用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;
}
从代码中我们看到MeasureSpec.AT_MOST,MeasureSpec.EXACTLY情况下。getDefaultSize返回的就是measureSpec中的SpecSize。SpecSize就是测量的大小。
MeasureSpec.UNSPECIFIED模式下view的大小返回的就是getSuggestedMinimumWidth()的大小。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
getSuggestedMinimumWidth()中的逻辑为,如果view没有设置背景则返回minWidth这个值,若如果设置了背景,则返回minWidth和背景最小宽度的的最大值。
(1)ViewGroup的measureViewGroup
ViewGroup提拱了一个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方法去测试子view的大小
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中获取child的Layoutparam,在调用getChildMeasureSpec去获取子view的MeasureSpec。最终调用measure的方法去测量出view的大小
二, layout过程
Layout的作用是确定布局的最终位置。view的layout方法用来确定view的位置。ViewGroup的onLayout方法用来确定每个子view的位置
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);
}
}
layout方法通过setFrame设定View的四个顶点位置。父布局位置确定后调用onlayout方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
onLayout方法是个空方法,没有具体的实现,需要子view自己去处理自己的位置逻辑。
三, draw过程
Draw的过程就是把确定好大小和位置的view绘制出来。
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);
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 (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
。。。。。
}
源码上注释也写的很清楚。总共分以下几步。
1. 绘制背景 drawBackground(canvas);
2. 绘制的内容 onDraw(canvas)
3. 绘制children dispatchDraw(canvas);
4. 绘制装饰 onDrawForeground(canvas);
5. 绘制默认焦点高亮 drawDefaultFocusHighlight
总结
1.想要自己绘制ViewGroup,需要实现onMeasure,在onMeasure方法里面遍历测量子元素。同理onlayout方法也一样。
2.绘制view需要在onMeasure方法里面处理下warp_content情况。不需要处理onlayout