自定义控件总结---onMeasure onLayout onDraw深入解析
对于一个Android开发来说,不管你有多不喜欢自定义控件,你都得和她谈一场恋爱。所以我主动谈了这场很费脑子的恋爱,因为阅读源码的能力不好,在寻根抛底的找线索的过程中,搞得反胃,不过最后还是把来龙去脉都捋了一遍。当然今天的角不是常用且高效的组合型自定义控件,而是纯粹绘制出来的View或者重新定义规则的一个ViewGroup。
简述1: 启动Activity的时候会创建一个PhoneWindow和DectorView,并将DectorView放进PhoneWindow中。这个DectorView(继承FrameLayout的控件View)的资源布局是根据Theme来确定的,这个布局一般是LinearLayout的上下布局,上面是一个title标题栏,下面是一个content的内容栏。我们一般在onCreate中的setContentView(resID),就是往这个内容栏里面放布局。
简述2:DectorView和PhoneWindow都有了,布局也setContentView了。 那么是谁启动的测量,布局,绘制这些绘制流程的呢? 答案是ViewRootImp这个类。 还是简单的捋一捋吧:
1.ActivityThread收到RESUME_ACTIVITY消息,执行handleResumeActivity()方法:将DectorView添加进Window中。
代码: wm.addView(decor, l);
2.然后会在WindowManagerGlobal类中调用addView方法:创建ViewrootImp,并将DectorView传进去。
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
3.然后在ViewRootImp的setView中触发requestLayout()方法,并执行scheduleTraversals();
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
4.然后scheduleTraversals()这个方法就是发送一个消息去执行一个线程,这个线程中便是真正开启执行测量,最终调用的是performTraversals方法:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
5.然后在这个方法里面调用测量 布局 绘制图 这些方法。performMeasure() performLayout() performDraw();
上面说了这么多就是为了更好的理解这个流程,而不至于一头雾水的留下为啥会调用这些方法,谁最先开启的这些方法等等有头无尾的疑问。
简述3:MeasureSpec是Mode和Size共同产生的一个值,32位。前两位是Mode模式,后面30位才是View实实在在的尺寸值。 一般情况下 一个View或者说ViewGroup的MeasureSpec是由父类ViewGroup和自己共同决定的。如下图:
1. EXACTLY :这个模式,精确模式,就是我能确定了它的具体值了。
a.给View 设置了具体的值,100dp 200dp 等
b.match_parenter:填充父窗体意思。如果父窗体是EXACTLY模式,即是一个确定的,那这个View就和他爹一样大。
2. AT_MOST :这个模式代表最大不超过某个值的模式。wrap_content.。自适应大小,但是最大不超过他爹的最大值。
3. UNSPECIFIED: 父容器不对View有任何限制,给它想要的任何尺寸。一般用于系统内部,表示一种测量状态。
一:测量
1. ViewRootImp进行测量的初始 performTraversals()此方法,这里面我们测量了根布局DectorView的宽和高childWidthMeasureSpec 和 childHeightMeasureSpec ,需要注意的是Dedctorview就是老祖宗,没有爹的限制,自己是啥就是啥。而实际上他的 宽高都是match_parent。所以就是EXACTLY+屏幕的尺寸。
对应的源码是:
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
2. 然后
-----> performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
-----> mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
-----> onMeasure(widthMeasureSpec, heightMeasureSpec);
3. onMeasure(widthMeasureSpec, heightMeasureSpec):首先判断这个View是否重写了onMeasure方法,如果重写了则调用重写的,否则就调用View.java 的。 这个方法View.java有,但是ViewGroup.java是没有的,因为继承他的自定义控件是一个容器,所以他的大小与子View的排列有关。因此继承ViewGroup 的自定义控件需要自己重写onMeasure来重新定义子View在其内部的排列规则。虽然View.java是有onMeasure方法,但是自己绘制的自定义View,他默认不处理padding设置的值,另外AT_MOST和MATCH_PARENT模式得到的尺寸是一样的,所以一般也是需要重写onmeasure()方法。下面我们就先看看View.java中的onMeasure()的源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
=====>根据measureSpec的mode来确定view真正的size(注意看了,上面我们说的WRAP_CONTENT和MATCH_PARENT模式得到的宽高尺寸是一样的,并且这个size是父窗体去掉自身的padding值后,最大可用尺寸,所以结果都是填充父窗体,所以得重写onMeasure方法,并且处理AT_MOST的这种情况)
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;
}
=======>将这个真正的size设置进去
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);
}
=====》在onLayout 中或者其他地方,我们想使用测量值的宽或者高的时候就可以调用:
view.getMeasuredWidth() 或者 view.geMeasureHeight()获取到我们设置进去的具体的实际尺寸值。
所以如果是绘制的自定义View,最多就是重写onMesure方法,对padding以及AT_MOST这种情况进行处理即可。 如果自定义的是ViewGroup 这种情况则需要重新写onMeasure方法,按自己的规则排列规则来设置其尺寸。
measureChild():测量的子View的宽高,是父ViewGroup去掉padding值后,给留给子View的最大可用尺寸。此时子View设置margin值会不起作用。
measureChildWithMargins():测量的子View的宽高,是父ViewGroup去掉其padding值以及子View的margin值后,给留给子View的最大可用尺寸。一般使用它,因为这样可控性会更好。
下面是ViewGroup提供的方法:
//遍历子View,并调用measureChild()方法对每个子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);
}
}
}
//父View去掉padding 之后的可用最大size值
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);
}
//父View 去掉padding以及子View的margin之后 可用的最大size值
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//下面这段源码 就是最上面的表格表达的内容:
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);
}
所以在自定义的ViewGroup容器的时候,需要重写onMeasure(),里面实现的东西是:
1.遍历子View-->2.子View调用measureChildWithMargins() 进行测量-->3.通过childView.getLayoutParams()获取这个子View的去除自身margin值之后的width,以及margin值--->4.然后再根据自定义容器的规则去计算宽高,并设置。
二:Layout 这个就简单,一般就是讲按规则测量的值设置即可,另外在onLayout()中将所有子View,按照规则进行排列设置即可。
1.performLayout-->host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());-->onLayout(changed, l, t, r, b); 规则也都是重写的就掉用重写的,没重写就调用View.java 中的,具体可以自行跟踪和measure类似。
2.如果重写,则遍历子View,获取每一个子View的宽高,margin值然后按规则进行计算,并且调用childView.layout(l,t,r,b)方法进行放置。
三:Draw也很简单:
performDraw()-->draw(fullRedrawNeeded);-->drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)--->mView.draw(canvas);--->View.java的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, 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;
}
下面我就简单的仿写一个Linearlayout:
/**
* 设计一个Linearlayout 包括水平,竖直情况(手动修改参数可以验证水平,竖直,原理一样的)。
*/
public class MyLinearLayout extends ViewGroup {
Context mContext;
// 布局方向,-1 水平 -2竖直
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
public static final int mOrientation = 0;
//自定义View的padding值
public int mPaddingLeft;
public int mPaddingRight;
public int mPaddingTop;
public int mPaddingBottom;
//子View的个数
public int childCount;
public int mTotalWidth = 0;//子View+方向 确定
public int mTotlaHeight = 0;//子View+方向 确定
public MyLinearLayout(Context context) {
this(context, null);
}
public MyLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
int widthMode;
int widthSize;
int heightMode;
int heightSize;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1.获取自定义ViewGroup的宽高的模式以及参考尺寸
//2.然后子根据子View的模式以及尺寸,来最终确定自定义ViewGroup的尺寸
widthMode = MeasureSpec.getMode(widthMeasureSpec);
widthSize = MeasureSpec.getMode(widthMeasureSpec);
heightMode = MeasureSpec.getMode(heightMeasureSpec);
heightSize = MeasureSpec.getMode(heightMeasureSpec);
mPaddingLeft = getPaddingLeft();
mPaddingRight = getPaddingRight();
mPaddingTop = getPaddingTop();
mPaddingBottom = getPaddingBottom();
childCount = getChildCount();
if (mOrientation == HORIZONTAL) {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
} else {
measureVertical(widthMeasureSpec, heightMeasureSpec);
}
}
private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = 0;
mTotlaHeight=0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth();
int childHigth = childView.getMeasuredHeight();
//水平方向(计算每一个子View的width并取最大的)
int cuurWeight = marginLayoutParams.leftMargin + childWidth + marginLayoutParams.rightMargin;
maxWidth = Math.max(maxWidth, cuurWeight);
//竖直方向计算(计算所有子View总的长度)
mTotlaHeight = mTotlaHeight+marginLayoutParams.topMargin + childHigth + marginLayoutParams.bottomMargin;
}
//计算完毕之后,再根据父类框架的Mode 来最终确定父类ViewGroup的宽高
if (widthMode == MeasureSpec.AT_MOST) {
//如果AT_MOST 我测量的所有子View加起来 就是父类的宽
maxWidth =maxWidth+ mPaddingLeft + mPaddingRight;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
}
if (heightMode == MeasureSpec.AT_MOST) {
//如果AT_MOST 我取子View中最大的高 作为父类的高
mTotlaHeight =mTotlaHeight+ mPaddingTop + mPaddingBottom;
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mTotlaHeight, MeasureSpec.EXACTLY);
}
//将处理好的在size和mode设置给自定义的ViewGroup里面
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
int maxHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth();
int childHigth = childView.getMeasuredHeight();
//水平方向计算(计算所有子View总的长度)
mTotalWidth += marginLayoutParams.leftMargin + childWidth + marginLayoutParams.rightMargin;
//竖直方向(计算每一个子View的height并取最大的)
int cuurHeight = marginLayoutParams.topMargin + childHigth + marginLayoutParams.bottomMargin;
maxHeight = Math.max(maxHeight, cuurHeight);
}
//计算完毕之后,再根据父类框架的Mode 来最终确定父类ViewGroup的宽高
if (widthMode == MeasureSpec.AT_MOST) {
//如果AT_MOST 我测量的所有子View加起来 就是父类的宽
mTotalWidth = mTotalWidth+mPaddingLeft + mPaddingRight;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTotalWidth, MeasureSpec.EXACTLY);
}
if (heightMode == MeasureSpec.AT_MOST) {
//如果AT_MOST 我取子View中最大的高 作为父类的高
maxHeight =maxHeight+ mPaddingTop + mPaddingBottom;
heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
}
//将处理好的在size和mode设置给自定义的ViewGroup里面
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == HORIZONTAL) {
layoutHorizontal(l, t, r, b);
} else {
layoutVertical(l, t, r, b);
}
layout(l,t,r,b);
}
private void layoutVertical(int l, int t, int r, int b) {
int mTop = mPaddingTop;
for (int i = 0; i < childCount; i++) {
View childAtView = getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams) childAtView.getLayoutParams();
int left = mPaddingLeft + layoutParams.leftMargin;
mTop = mTop + layoutParams.topMargin;
int right = left + childAtView.getMeasuredWidth();
int bottom = mTop + childAtView.getMeasuredHeight();
childAtView.layout(left, mTop, right, bottom);
mTop =bottom+ layoutParams.topMargin;
}
}
private void layoutHorizontal(int l, int t, int r, int b) {
int mleft = mPaddingLeft;
for (int i = 0; i < childCount; i++) {
View childAtView = getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams) childAtView.getLayoutParams();
mleft = mleft + layoutParams.leftMargin;
int top = mPaddingTop + layoutParams.topMargin;
int right = mleft + childAtView.getMeasuredWidth();
int bottom = top + childAtView.getMeasuredHeight();
childAtView.layout(mleft, top, right, bottom);
mleft = right+layoutParams.rightMargin;
}
}
/**
* 重写获取的默认的LayoutParams,否则或出现强转异常
*
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MyLayoutParams(lp);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
public static class MyLayoutParams extends MarginLayoutParams {
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public MyLayoutParams(int width, int height) {
super(width, height);
}
public MyLayoutParams(LayoutParams lp) {
super(lp);
}
}
}
总结:本篇很长,因为你涉及了一些源码。主要分成两大部分:
1.Activity的内部视图结构以及如何去触发测量,布局,绘制的。
2.具体分析测量,布局,绘制流程。
注意事项:
1.measureChild()和measureChildWithmargin()区别,以及如何使用。上面给出了解释。
2.如果使用measureChildWithmargin()需要重写getLayoutparams(),否则会类型转换异常。
3.系统默认不处理padding值,WRAP_CONTENT和MATCH_PARENET得到的结果是一样的,所以需要重写onMeasure()进行处理。
4. getMeasureWidth()或者getMeasureHeight.() 得到的尺寸是onMeasure测量尺寸。
getWidth()和getHeight()获取的是onLayout()设置位置之后的尺寸,也就是显示在屏幕中的最终尺寸。
getWidth=right-left;