自定义控件(View的绘制流程源码解析)

时间:2021-04-29 23:21:49
一直以来我们都是在用Andriod自带的控件,比如TextView,ImageView等来构建我们的应用,有的时候并不能满足我们的需要,接下来就来研究一下如何自定义我们自己的控件,当然自己造新的*,自然先要搞懂系统自带的*的实现原理,下面将从源码的角度深入分析view的绘制流程,进而来分析一下如何自定义新的控件:
下面首先从源码的角度来分析一下view的绘制流程:
先看一下view树的结构图:
自定义控件(View的绘制流程源码解析)
自定义控件(View的绘制流程源码解析)
可以看到android中任何一个布局或控件都是直接或间接继承于view实现的,对于我们后面要自定义的控件自然也不例外,所以想要自定义控件首先必须得先搞明白android自带的view布局和控件到底是如何绘制并显示到屏幕上的。
1、既然要把view绘制到屏幕上,那自然首先要搞清楚绘制是从哪开始的?
整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法开始的,这个方法很长,我们抽取主要的代码逻辑展示出来:
 childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
      ......
 /**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看到该方法内部会调用view的measure方法,getRootMeasureSpec方法中传入了两个参数,其中lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT,也就是说在ViewGroup实例创建的时候就被赋值了
那这里lp是什么呢?    WindowManager.LayoutParams lp = mWindowAttributes;  (注: WindowManager.LayoutParams 是 WindowManager 接口的嵌套类;它继承于 ViewGroup.LayoutParams; 它用于向WindowManager描述Window的管理策略。)实际上lp即为屏幕参数,从getRootMeasureSpec方法的实现中可以看出无论rootDimension是MATCH_PARENT或WRAP_CONTENT,最后都会强制将root view转换为全屏的,查看上面的case语句分支可以知道通过调用makeMeasureSpec方法根据传入的specSize:windowSize和specMode参数对MeasureSpec重新封装,尺寸全部为windowSize,所以根视图总是全屏的
至此我们也便明白了最外层根视图的widthMeasureSpec和heightMeasureSpec是从哪里来的了。
接下来就是对view的绘制流程,下面就是绘制流程图:
自定义控件(View的绘制流程源码解析)
 首先看一下measure源码:
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
} // Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} // flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
} mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
} mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

仔细看measure方法声明可以看到该方法是final的,也就是子view不能对其进行重写,根据注释可以知道:为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measure方法是final的,不允许重载,所以View子类只能通过重载onMeasure来实现自己的测量逻辑

该方法中的两个参数都是父类传递进来的,表示父类的规格;

下面看一下View的onMeasure源码:

 /**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

代码中的注释对于onMeasure的重写有详细的说明,该方法是由measure触发的,子类必须重写以提供精确而有效的测量

当重写该方法时必须调用setMeasuredDimension(int, int)来存储这个view测量的宽和高,否则会报异常,调用超类的onMeasure方法可以正常运行,不过是按照系统即超类的测量方式进行的自定义控件(View的绘制流程源码解析)

对于非ViewGroup的view,通过调用上面默认的onMeasure就可以实现view的测量,也可以重载onMeasure并调用setMeasuredDimension来设置任意大小的布局,但建议一般不这样做,后面会分析原因

从上面的代码可以看出,默认的onMeasure只实现了调用setMeasuredDimension方法,该方法对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束

默认实现中参数是通过调用getDefaultSize方法来传递的,接下来看一下该方法的源码:

     public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//通过MeasureSpec解析获取mode与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等于AT_MOST或EXACTLY就返回specSize,这是系统默认的格式

回过头继续看上面onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,具体如下:

  /**
* 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());
} protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }

可以看到:建议的最小宽度和高度都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的

到这里就完成了view的measure过程

对于一个界面可能包括很多次measure,因为对于一个布局而言,一般会包括很多子view,每个子视图都需要measure,根据view的继承结构可以知道可以嵌套的view都是继承于ViewGroup的,故而在ViewGroup中定义了很多方法:measureChildren, measureChild, measureChildWithMargins,来对子视图进行测量,measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小

下面首先来看一下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);
}
}
}

可以看到循环遍历子视图,然后第7行调用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);
}

可以看到,在第4行和第6行分别调用了getChildMeasureSpec()方法来去计算子视图的MeasureSpec,计算的依据就是布局文件中定义的MATCH_PARENT、WRAP_CONTENT等值,这个方法的内部细节就不再贴出;然后在第8行调用子视图的measure方法,并把计算出的MeasureSpec传递进去,这样就和前面分析单独view的测量过程是一样的了,至此就可以明白整个测量过程了,接着再来看一下measureChilldWithMargins源码:

  /**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子视图的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//调整MeasureSpec
//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
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);
//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看出和上面的measureChild方法类似,唯一不同的就是加上了左右边距margin,刚才前面没有对getChildMeasureSpec方法源码进行分析,这里为了全面理解,还是来查看一下源码:

   public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取当前Parent View的Mode和Size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
int size = Math.max(0, specSize - padding);
//定义返回值存储变量
int resultSize = 0;
int resultMode = 0;
//依据当前Parent的Mode进行switch分支逻辑
switch (specMode) {
// Parent has imposed an exact size on us
//默认Root View的Mode就是EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果child的layout_width属性在xml或者java中给予具体大于等于0的数值
//设置child的size为真实layout_width属性值,mode为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果child的layout_width属性在xml或者java中给予MATCH_PARENT
// Child wants to be our size. So be it.
//设置child的size为size,mode为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//如果child的layout_width属性在xml或者java中给予WRAP_CONTENT
//设置child的size为size,mode为AT_MOST
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
......
//其他Mode分支类似
}
//将mode与size通过MeasureSpec方法整合为32位整数返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以看见,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension。

所以可以看见onMeasure的参数其实就是这么计算出来的。同时从上面的分析可以看出来,最终决定View的measure大小是View的setMeasuredDimension方法,所以我们可以通过setMeasuredDimension设定死值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是一个好的自定义View应该会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,这样的灵活性更大,所以这也就是上面分析onMeasure时说View的onMeasure最好不要重写死值的原因

可以看见当通过setMeasuredDimension方法最终设置完成View的measure之后View的mMeasuredWidth和mMeasuredHeight成员才会有具体的数值,所以如果我们自定义的View或者使用现成的View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值

通过上面的分析,其实view的measure过程就是从顶层向下依次测量子视图的过程,即调用子视图的measure方法(measure又会回调onMeasure方法)。

核心点:

  • MeasureSpec(View的内部类)测量规格为int型,值由高16位规格模式specMode和低16位具体尺寸specSize组成。其中specMode只有三种值:
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;
  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  • View的布局大小由父View和子View共同决定。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

由上综述即分析完了整个测量过程

接下来我们来分析layout的过程:

首先大致说一下layout的执行过程:子视图的具体位置是相对于父视图的。View的onLayout是一个空方法,而ViewGroup的onLayout是一个抽象方法,所以如果自定义的View要继承于ViewGroup就必须重写该方法

由最开始的ViewRootImpl的performTraversals方法中可以看出在measure执行完毕后就会执行layout:

mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

这里先来看一下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 = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
}

第6行调用setFrame方法,传入新的view坐标参数,返回一个boolean值,用来决定是否改变了,如果改变了就需要重新布局,第11行调用onLayout,到这里你会发现和上面分析measure的过程很类似,也是会回调,接着我们就来看一下onLayout的源码,结果会发现方法实现为空:因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置

那我们再来看一下ViewGroup的onLayout方法实现,你会发现是个抽象方法

protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

所以ViewGroup的所有子视图都必须重写该方法,事实上,LinearLayout和RelativeLayout都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的

那既然这样,我们要知道onLayout的编写实现就需要来参考一下系统控件的实现了,这里以LinearLayout为例:

 @RemoteView
public class LinearLayout extends ViewGroup { @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);
}
}
.....
}

可以看到对于LinearLayout的onLayout方法内部会根据布局方向来调用不同的方法,这也正说明了我们再使用LinearLayout时设置方向了

这里我们就来分析Vertical的情况吧:

 /**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
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 计算父窗口推荐的子View宽度
final int width = right - left;
int childRight = width - mPaddingRight; //计算父窗口推荐的子View右侧位置 // Space available for child child可使用空间大小
int childSpace = width - paddingLeft - mPaddingRight;
// 通过ViewGroup的getChildCount方法获取ViewGroup的子View个数
final int count = getVirtualChildCount();
     // 获取Gravity属性设置
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//依据majorGravity计算childTop的位置值
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;
}
//开始遍历整个ViewGroup
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {//LinearLayout中其子视图显示的宽和高由measure过程来决定的
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//获取子View的LayoutParams
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) { //依据不同的absoluteGravity计算childLeft位置
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;////通过垂直排列计算调用child的layout设置child的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i);
}
}
}

从上面分析的ViewGroup子类LinearLayout的onLayout实现代码可以看出,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子View的具体位置。

到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别(上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效)。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。那我们看下View源码中这些方法的实现吧,如下:

 public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
} public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
} public final int getWidth() {
return mRight - mLeft;
} public final int getHeight() {
return mBottom - mTop;
} public final int getLeft() {
return mLeft;
} public final int getRight() {
return mRight;
} public final int getTop() {
return mTop;
} public final int getBottom() {
return mBottom;
}

layout总结:

整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。

  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值

那分析了layout的具体原理,那接下来附上一个简单的实例来看一下实际应用中如何使用onLayout:这一块实例是引用郭大神的博文中的

 public class SimpleLayout extends ViewGroup {

     public SimpleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}

}

上面的例子只包含一个子view,onLayout的实现也很简单,就是判断子视图个数不为空,然后获取子视图,根据measure测量的宽高直接调用layout方法即可

具体显示的位置可以*改变layout中的参数

最后来分析draw的详细过程:

由最开始的ViewRootImpl的performTraversals()方法可以看出在执行完measure和layout之后就要执行draw方法了(ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作),由前面measure和layout的比较我们知道两者整体执行过程很相似,都会在递归执行measure或layout 之中回调相应的onMeasure或onLayout方法,于是到这里我们可以猜想draw是不是也类似呢?实际上draw的过程要相对复杂一些,除了会调用onDraw方法之外,还有一些其他的方法需要调用,接下来我们就源码来具体分析一下:
这里我们首先来画一下整个view树结构的遍历过程:
自定义控件(View的绘制流程源码解析)

接下来先来看一下draw的源码:代码较长

    /**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
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); // 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); // we're done...
return;
} /*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/ boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false; float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
} int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired); if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
} final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight; // clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
} // also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
} if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
} if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
} saveCount = canvas.getSaveCount(); int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
} if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
} if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
} if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
} // 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; if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
} if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
} if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
} if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
} canvas.restoreToCount(saveCount); // 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);
}

代码注释中已经详细描述了绘制的步骤:

/*
18 * Draw traversal performs several drawing steps which must be executed
19 * in the appropriate order:
20 *
21 * 1. Draw the background
22 * 2. If necessary, save the canvas' layers to prepare for fading
23 * 3. Draw view's content
24 * 4. Draw children
25 * 5. If necessary, draw the fading edges and restore layers
26 * 6. Draw decorations (scrollbars for instance)
27 */ 源码注释中提到步骤2和5可以跳过,那我们接下来主要分析剩下4步:

第一步:绘制背景

主要逻辑在方法drawBackground(canvas);中,分析一下源码:

  /**
* Draws the background onto the specified canvas.
*
* @param canvas Canvas on which to draw the background
*/
private void drawBackground(Canvas canvas) {
      //获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
final Drawable background = mBackground;
if (background == null) {
return;
}
//根据layout过程确定的View位置来设置背景的绘制区域
setBackgroundBounds(); // Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode); final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
} final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);//调用Drawable的draw()方法来完成背景的绘制工作
canvas.translate(-scrollX, -scrollY);
}
}

简要分析:

这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值

2、对View内容的绘制

从代码中看出就是调用onDraw(canvas)

这里就是一个空方法,需要子类去实现,因为每个子视图的所要绘制的内容都不一样

3、对当前View的所有子view进行绘制,如果没有则不绘制

dispatchDraw(canvas);

这里是个空方法,对单个view而言是没有子view的,也就没有实现,这里可以看一下ViewGroup中该方法的实现:
由于代码较长,这里只抽取主要的逻辑代码:

 @Override
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
}

可以看到ViewGroup确实重写了dispatchDraw方法,从第17行可以看出会循环遍历子view,然后调用drawChild方法绘制子view,而drawChild方法内部其实就是调用draw方法,至此我们搞清楚了ViewGroup已经为我们重写了dispatchDraw的功能实现,所以一般不用重写该方法,但是可以重载父类方法实现具体的功能

到这里我们可以明白,如果我们要让ViewGroup绘制的话,就可以在两个地方进行:1、将绘制逻辑添加到重写的dispatchDraw方法中,但是在该方法中首先要调用超类的该方法,即super.dispatchDraw(canvas);然后才能执行其他的绘制逻辑

2、直接重写onDraw方法,将绘制逻辑添加在该方法内

对于方法2,针对ViewGroup有一点要注意:

The second is using the same onDraw() callback as we saw before with View. Anything we draw here will be drawn before the child views, and thus will show up underneath them. 首先绘制,位于子view的下面 This can be helpful for drawing any type of dynamic backgrounds or selector states.适合绘制任何类型的动态背景和选择器状态

If you wish to put code in the onDraw() of a ViewGroup, you must also remember to enable drawing callbacks with setWillNotDraw(false). Otherwise your onDraw() method will never be triggered. This is because ViewGroups have self-drawing disabled by default.

调用onDraw之前需要调用setWillNotDraw(false)方法使能drawing回调,因为ViewGroup默认是禁止自绘的

 

4、然后就是第6步,也就是最后一步,对视图的滚动条进行绘制。我们要明白其实所有的view都是有滚动条的,只是一般情况我们都隐藏掉了

这一块不是重点,代码也比较多,就不做过多分析了

综上,其实这里的重点就是上面的第2点,对内容的绘制,也即调用onDraw。这里面会有很多的绘制逻辑,绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用

这里我们先总结一下draw的整个过程,然后再着重来研究一下onDraw的实现:
这里引用工匠若水的博文:

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

  • View的绘制是借助onDraw方法传入的Canvas类来进行的

  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

对于onDraw的简单实用,这里贴上一个样例:

 public class MyView extends View {

     private Paint mPaint;

     public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
} @Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String text = "Hello View";
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
}

至此,我们便将测量,布局,绘制三个大的自定义view的步骤详细分析了一遍

下面来分析一下invalidate方法:注:该方法只能在UI线程中调用
View的invalidate方法最终都会归于调用invalidateInternal方法,这里贴上源码:

 void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
......
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//设置刷新区域
damage.set(l, t, r, b);
//传递调运Parent ViewGroup的invalidateChild方法
p.invalidateChild(this, damage);
}
......
}

第12行会调用父类的invalidateChild方法,View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集

这里查看一下invalidateChild的源码:

 public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
......
do {
......
//循环层层上级调运,直到ViewRootImpl会返回null
parent = parent.invalidateChildInParent(location, dirty);
......
} while (parent != null);
}

这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法

    @Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
......
//View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
scheduleTraversals();
......
return null;
}

这个ViewRootImpl类的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中说的,层层上级传递到ViewRootImpl的invalidateChildInParent方法结束了那个do while循环。看见这里调运的scheduleTraversals这个方法吗?scheduleTraversals会通过Handler的Runnable发送一个异步消息,调运doTraversal方法,然后最终调用performTraversals()执行重绘。开头背景知识介绍说过的,performTraversals就是整个View数开始绘制的起始调运地方,所以说View调运invalidate方法的实质是层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals方法,然后整个View树开始重新按照上面分析的View绘制流程进行重绘任务

到此View的invalidate方法原理就分析完成了

对于非U线程需要使用postInvalidate方法:

这里不做具体分析了,只要明白一点就好,最后该方法的调用最终会回到UI线程中调用invalidate方法

接下来总结一下:这里也是参考工匠若水的博文

invalidate系列方法请求重绘View树(也就是draw方法),如果View大小没有发生变化就不会调用layout过程,并且只绘制那些“需要重绘的”View,也就是哪个View(View只绘制该View,ViewGroup绘制整个ViewGroup)请求invalidate系列方法,就绘制该View。

常见的引起invalidate方法操作的原因主要有:

  • 直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。
  • 触发setSelection方法。请求重新draw,但只会绘制调用者本身。
  • 触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。
  • 触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。
  • 触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。