Android中的任何一个布局、控件都是直接或间接继承自View的,要想开发一个Android App,就肯定少不了要和View打交道。尽管Android已经提供了丰富的控件和布局,但是要想开发出有自己特色的App,自定义View是必须要掌握的。那么,今天我们就来聊聊View绘制的相关内容。
本文的要点如下:
- View简介
- MeasureSpecs类
- onMeasure
- 单一View的测量
- ViewGroup的测量
- onLayout
- 单一View的layout过程
- ViewGroup的layout过程
- onDraw
- 总结
View简介
在Android系统中View是所有控件的基类,其中也包括ViewGroup在内,ViewGroup是代表着控件的集合,其中可以包含多个View控件。
从某种角度上来讲Android中的控件可以分为两大类:View与ViewGroup。通过ViewGroup,整个界面的控件形成了一个树形结构,上层的控件要负责测量与绘制下层的控件,并传递交互事件。
在每棵控件树的顶部都存在着一个ViewParent对象,它是整棵控件树的核心所在,所有的交互管理事件都由它来统一调度和分配,从而对整个视图进行整体控制,如下图所示:
绘制出整个界面肯定是要遍历整个View树,对这棵树的所有节点分别进行测量,布局和绘制。万事皆有源头,绘制得从根节点顶级View开始画起,即DecorView。
系统内部会依次调用DecorView的measure,layout和draw三大流程方法。measure方法又会调用onMeasure方法对它所有的子元素进行测量,如此反复调用下去就能完成整个View树的遍历测量。同样的,layout和draw两个方法里也会调用相似的方法去对整个View树进行遍历布局和绘制。
View的构造函数:共有4个,具体如下:
// 如果View是在Java代码里面new的,则调用第一个构造函数
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
View的位置参数:
View的位置由4个顶点决定(View的位置是相对于父控件而言的)
- Top:子View上边界到父view上边界的距离
- Left:子View左边界到父view左边界的距离
- Bottom:子View下边距到父View上边界的距离
- Right:子View右边界到父view左边界的距离
MeasureSpecs类
MeasureSpecs类是View的内部类,用一个变量封装了两个数据(size、mode),其目的是减少对象的内存分配。
public static class MeasureSpec {
//省略了部分不关键代码
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
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;
// 通过Mode 和 Size 生成新的SpecMode
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取测量模式(Mode)
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
//原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
//例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST)
//这样就得到了mode的值
}
//获取测量大小(Size)
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),
//将32,31位替换成0也就是去掉mode,保留后30位的size
}
}
测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)。
其中,测量模式占最高两位,测量大小则是MeasureSpec的低30位。测量模式(Mode)的类型有3种:UNSPECIFIED、EXACTLY 和AT_MOST。具体如下:
UNSPECIFIED:父View不约束子View(即子View可以获取任意尺寸),多用于系统内部View(ListView,ScrollView等),自定义View一般用不到。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
EXACTLY(精确模式):父View会为子View指定一个确切尺寸,子View必须在该尺寸之内。对应LayoutParams中的match_parent或具体数值。
AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。
onMeasure
View的绘制流程中,第一步就是测量,即onMeasure()方法。我们知道,自定义View的类型可以分为两种,一种继承View,一种继承ViewGroup(继承现有View的也可以根据继承的View的不同归入这两种类型之中)。那么测量的过程也有两种:
- 单一View的测量
- ViewGroup的测量
那么下面我们就分别来看看两种流程的不同。
单一View的measure过程
这种情况相对而言比较简单,不用考虑子View,只有一个原始的View,通过measure()即可完成测量,具体过程如下:
那么我们来看看整个流程的源码:
//measure是测量流程的开始,由于是final类型,因此不能被重写,
//主要是用来进行基本测量逻辑的判断。
//里面调用onMeasure进行测量逻辑。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);//具体的测量逻辑
} else {
...
}
measure方法是测量过程中最先调用的方法,View的这个方法是被它的父控件调用的。由于measure是final类型,不能被子类重写,那么就只能重写onMeasure方法来实现测量逻辑了。
//在onMeasure中就做了两件事,
//1是根据View宽/高的测量规格用getDefaultSize()方法计算View的宽/高值,
//2是用setMeasuredDimension()方法存储测量后的View宽 / 高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//如果有设置背景,则获取背景的宽度,如果没有设置背景,则取xml中android:minWidth的值。
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
onMeasure里面用到了getSuggestedMinimumWidth和getSuggestedMinimumHeight,这两个方法差不多的,我们就以getSuggestedMinimumWidth为例。mMinWidth属性对应的就是xml布局里的android:minWidth属性,设置最小宽度。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始宽度,这个宽度跟背景的类型有关。比如我们给View的背景设置一张图片,那这个方法返回的宽度就是图片的宽度,而如果我们给View背景设置的是颜色,那么这个方法返回的宽度则是0。
所以,这个方法的返回的宽度是:如果View没有设置背景,那就返回xml布局里的android:minWidth属性定义的值,默认为0;如果View设置了背景,就返回背景的宽度和mMinWidth中的最大值。
//存储测量后的View宽 / 高,该方法即为我们重写onMeasure()所要实现的最终目的。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//判断该view布局模式是否有一些特殊的边界
boolean optical = isLayoutModeOptical(this);
////判断view和该view的父view的布局模式情况,如果两者不同步,则进行子view的size大小的修改
if (optical != isLayoutModeOptical(mParent)) {
//有两种情况会进入到该if条件,
//一是子view有特殊的光学边界,而父view没有,此时optical为true,
//一种是父view有一个特殊的光学边界,而子view没有,此时optical为false
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
//存储测量后的View宽 / 高的实际逻辑
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
//根据View宽/高的测量规格计算View的宽/高值
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:
// 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
//模式为AT_MOST,EXACTLY时,
//使用View测量后的宽/高值 = measureSpec中的Size
result = specSize;
break;
}
return result;
}
可以看出,View在当测量模式为UNSPECIFIED时,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小。其实这对我们自定义控件并没有什么影响,因为UNSPECIFIED模式是给系统内部用的。我们的重点还是应该放在AT_MOST和EXACTLY两种情况下。对于这两种情况,getDefaultSize十分简单粗暴,直接返回了specSize,也就是View的测量规格里的测量尺寸。
这里就出现了一个问题在AT_MOST和EXACTLY两种情况下返回的尺寸竟然都是specSize。
因此在自定义View控件时,我们需要重写onMeasure方法并设置wrap_content时自身的大小。否则在xml布局中使用wrap_content时与match_parent的效果将会是一样。
至此,单一View的宽/高值已经测量完成,即对于单一View的measure过程已经完成。
小小的总结一下,其是前面的源码只是为了对View的测量有个完整的概念,清楚整个流程,主要我们实现还是在onMeasure方法中,因此可以写一个自定义View的onMeasure方法的通用模版,其实最关键的也就是在AT_MOST模式时进行特殊处理,毕竟父类的onMeasure已经实现了大部分逻辑:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 必须调用,因为父类还是实现了很多东西的。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 宽的测量模式
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
// 宽的测量尺寸
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
// 高度的测量模式
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// 高度的测量尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//根据View的逻辑得到,比如TextView根据设置的文字计算wrap_content时的大小。
//这两个数据变量要根据实现需求计算。
int wrapWidth,wrapHeight;
// 如果有测量模式是AT_MOST则需要进行特殊处理
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
//长宽都是AT_MOST,则都需要计算
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
//只有宽是AT_MOST,则长用测量尺寸
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
//只有长是AT_MOST,则宽用测量尺寸
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}
ViewGroup的measure过程
ViewGroup的情况就复杂一些了,毕竟它还有一堆子View要考虑,大多数时候要先确定子View的大小,再确定ViewGroup的的大小。不过原理也很简单,就是遍历测量所有子View的尺寸然后将所有子View的尺寸进行合并,最终得到ViewGroup父视图的测量值。
看过源码就知道ViewGroup并没有重写View的onMeasure方法,为什么呢?显然这需要它的子类去根据相应的逻辑去实现,比如LinearLayout与RelativeLayout对child View的测量逻辑显然是不同的。这个也是单一View的measure过程与ViewGroup过程最大的不同。
不过,ViewGroup倒是提供了一个measureChildren的方法,貌似可以用来测量child的样子,看看源码:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;//子View集合
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
这个方法的逻辑就很清晰嘛,就是遍历子View,调用measureChild方法对其进行测量,那么我们接着来看看measureChild方法:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//取出子View的LayoutParams
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的LayoutParams,再结合父控件的测量规格和已被占用的空间Padding,作为参数传递给getChildMeasureSpec方法,在getChildMeasureSpec里会组合生成child控件的测量规格。
getChildMeasureSpec是ViewGroup里提供的一个静态方法,用来用来获取子控件的测量规格。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view的测量模式
int specMode = MeasureSpec.getMode(spec);
//父view的测量大小
int specSize = MeasureSpec.getSize(spec);
//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
/// 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果child的布局参数有固定值(大于0),比如"layout_width" = "100dp"
//那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//当子view的LayoutParams为MATCH_PARENT时(-1)
//此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//当子view的LayoutParams为WRAP_CONTENT时(-2)
//比如TextView根据设置的字符串大小来决定自己的大小
//那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
//所以测量模式就是AT_MOST,测量大小就是父控件的size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父控件的测量模式是AT_MOST时,父view强加给子view一个最大的值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也会优先满足孩子的需求
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
//但同样的,child的尺寸上限也是父控件的尺寸上限size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child想要根据自己逻辑决定大小,那就自己决定呗
//同样的,child的尺寸上限也是父控件的尺寸上限size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
// 多见于ListView、GridView
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的测量流程也基本结束了,整体的流程如下图:
不过还有一个问题不知道大家注意到没有,measureChild方法只考虑了父View的padding,但是没考虑到子View的margin。这就会导致子view在使用match_parent属性的时候,margin属性会有问题。
当然,ViewGroup也考虑到了这个问题,为此也提供了另一个测量child的方法measureChildWithMargins:
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);
}
measureChildWithMargins方法,根据名字也能看出来,比measureChild方法多考虑了个margin,源码也跟前面的差不多,只是将margin考虑了进去,所以一般情况下,这个方法使用的更多一些。
至此,自定义View的中最重要、最复杂的measure过程就全部总结完了。下面就该Layout过程了。
onLayout
类似measure过程,layout过程根据View的类型分为2种情况,单一View和ViewGroup。对于单身View来说,一人吃饱全家不饿,调用layout方法确定好自己的位置,设置好位置属性的值(mLeft/mRgiht,mTop/mBottom)就行。而对于父母ViewGroup来说,还得通过调用onLayout方法帮助孩子们确定好位置。
单一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;
//调用setFrame / setOpticalFrame方法来给View的四个顶点属性赋值,
//即mLeft,mRight,mTop,mBottom四个值
//判断当前View大小和位置是否发生了变化 & 返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用onLayout方法
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;
//监听View位置变化
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
尽管代码有点长,不过不难看出layout方法首先会调用isLayoutModeOptical这个方法,判断是否有光学边界的(光学边界这里暂时用不到,其实关键是我也不会,想深入了解的请自行谷歌),之后调用setFrame或者setOpticalFrame方法来给View的四个顶点属性赋值,即mLeft,mRight,mTop,mBottom四个值。那么我们再来看看setOpticalFrame方法:
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
可以看到,这个setOpticalFrame方法,最终也是调用了setFrame,那好我们可以直接继续看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);
//省略部分代码
return changed;
}
不难看出,setFrame中先比较了新位置和老位置是否有差异,如果有差异则会调用sizechanged来更新View的位置。
setFrame后这个View的位置就确定了。之后我们也就能通过调用getWidth()和getHeight()方法来获取View的实际宽高了。
然后,才会调用onLayout方法,由于单一View是没有子View的,因此在View类里的onLayout方法是个空方法。
另外,在layout方法的最后我们能看到一个OnLayoutChangeListener的集合,光看名字我们也知道,这是View位置发生改变时的回调接口。所以我们可以通过addOnLayoutChangeListener方法可以监听一个View的位置变化,并做出想要的响应。(不看源码根本不知道还有这样的方法。。。)
ViewGroup的layout过程
ViewGroup的layout过程就比View复杂一些了,大致分为两步,首先用layout方法计算自身ViewGroup的位置,之后在onLayout中遍历子View并且确定自身子View在ViewGroup的位置(调用子View 的 layout方法)。
其实,ViewGroup的Layout的关键就是实现onLayout方法,在ViewGroup中onLayout方法被声明成了抽象方法,这就强制继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。
由于onLayout方法之前的流程和View是一样的,因此就不再赘述了。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置
// 遍历子View:循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 计算当前子View的四个位置值
//位置的计算逻辑
//TODO
// 需自己实现,也是自定义View的关键
// 对计算后的位置值进行赋值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
// 即确定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
}
}
}
onDraw
终于,在测量完毕,布局完成之后,我们来到了View绘制的最后一步,那就是将View绘制到屏幕上。不论是View还是ViewGroup,都是调用draw方法完成绘制,我们来看看draw的源码:
public void draw(Canvas canvas) {
//省略部分代码
int saveCount;
// 步骤1: 绘制本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步骤2:绘制本身View内容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:默认为空实现,需复写
// ViewGroup中:需复写
// 步骤3:绘制子View
// 由于单一View无子View,故View 中:默认为空实现
// ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
dispatchDraw(canvas);
// 步骤4:绘制装饰,如滑动条、前景色等等
onDrawScrollBars(canvas);
return;
}
//省略部分代码
}
和Measure以及Layout过程一样,View和ViewGroup是有些区别的,不过根据draw方法的源码来看,整体流程都是下面几个步骤:
- 绘制背景 -- drawBackground()
- 绘制自己 -- onDraw()
- 绘制孩子 -- dispatchDraw()
- 绘制装饰 -- onDrawScrollbars()
View和ViewGroup最大的差别就是dispatchDraw()方法,由于View中不用考虑子View,那么dispatchDraw()就是一个空实现,而ViewGroup则必须要实现dispatchDraw()。
那么我们再来一步一步看看源码中都做了什么:
1. 绘制背景 -- drawBackground()
private void drawBackground(Canvas canvas) {
//mBackground是该View的背景参数,比如背景颜色
// 获取背景 drawable
final Drawable background = mBackground;
//没有背景则直接结束方法
if (background == null) {
return;
}
//根据在 layout 过程中获取的 View 位置的四个参数来确定背景的边界
setBackgroundBounds();
//省略部分代码
//获取当前View的mScrollX和mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
// 调用 Drawable 的 draw 方法绘制背景
background.draw(canvas);
} else {
//如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
canvas.translate(scrollX, scrollY);
// 调用 Drawable 的 draw 方法绘制背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
2.绘制自己 -- onDraw()
由于不同的控件都有自己不同的绘制实现,所以View的onDraw方法肯定是空方法。在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容。也就是说我们在自定义View的时候要根据实际需求对onDraw方法进行实现。
3.绘制孩子 -- dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
......
// 1. 遍历子View
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 绘制子View视图
more |= drawChild(canvas, transientChild, drawingTime);
}
....
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
不难看出,dispatchDraw的逻辑就是遍历绘制子View,即遍历调用drawChild方法,drawChild方法又调用了child的draw(canvas, this, drawingTime)方法,最后还是调用到了child的draw(canvas)方法,这样绘制流程也就一层一层的传递下去了。
4.绘制装饰 -- onDrawScrollbars()
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
这一步的目的是绘制装饰,如 滚动指示器、滚动条、和前景等。
至此,View的draw过程分析完毕。
总结
View的绘制流程可以总结为下图:
从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。尽管源码中调用的方法很多,但是这些方法其实不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。
onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法。
图片来源:Carson_Ho的自定义View
由于本人水平有限,若是文中有叙述不清晰或不准确的地方,希望大家能够指出,谢谢大家!