面试官装逼失败之View的绘制流程
当我们打开手机,开始看朋友圈,刷微博的时候,我们有考虑过在我们眼前的一个个View
是如何从无到有的展示在我们眼前的么?有考虑过它们的感受么?(神经病才去考虑(ノಠಠ)ノ彡┻━┻......)。当我们兴致勃勃的去参加面试, 面试官上来就问:平时做过自定义过控件吗,OK做过,那说一下view的绘制流程吧。word天, 脑袋一片空白?不知从何说起?┭┮﹏┭┮,面试官成功的装了一波逼, 气势上就输了一半了。别怕,今天我们就来谈一谈view的绘制流程吧!
1.前言
大家应该都听过画饼充饥的故事吧,这里就不讲故事了。不知你们有没有想过,想要画一张饼出来,解解馋,要考虑哪些问题呢?个人觉得,不得不考虑下面几点:
- 这张饼我要画多大呀?画多大才够看过瘾呢?不能大的超过纸的范围吧?
- 这张饼画在纸的哪里呢?是纸的中间还是靠下面一点呢?画哪里好吃一点呢(滑稽)!
- 现在确定好大小和位置了,该怎么画呢?画成圆形的饼吗?加点什么调料(颜色)?
其实在手机屏幕上“画”一个View跟上述的流程也很相似。同样是经过了测量流程、布局流程以及绘制流程。我们都知道,Android
界面布局是以一棵树的结构形式展现的,看我们的xml布局文件也看的出来。而绘制出整个界面肯定是要遍历整个View树,对这棵树的所有节点分别进行测量,布局和绘制。万事皆有源头,绘制得从根节点顶级View开始画起,即DecorView
。至于啥是DecorView,大家可以自行去查阅资料。
系统内部会依次调用DecorView的measure
,layout
和draw
三大流程方法。measure方法又会调用onMeasure
方法对它所有的子元素进行测量,如此反复调用下去就能完成整个View树的遍历测量。同样的,layout和draw两个方法里也会调用相似的方法去对整个View树进行遍历布局和绘制。掌握这三个流程方法,也就基本掌握了view的绘制流程啦、是不是有点小期待!
2.测量流程-measure
测量流程的话得分情况来看,如果是单身View
,那自然是没话说,自己照顾好自己,本分的测量好自己就行。而如果是为人父母的ViewGroup
,那就得顾家了,除了测量好自己,还得去调用孩子们的measure方法让孩子们都测量好自己。甚至很多时候,ViewGroup得先测量好孩子们,最后才能确定自己的测量大小。一把辛酸泪...(ノへ ̄、)。
所以今天我们有必要从View和ViewGroup两个方面来了解测量过程:
2.1 View的测量过程
既然要了解View的测量measure过程,无疑源码是最好的解释。View类的measure方法的签名如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
首先看到这个方法,我得提出两个问题:
- 形参
widthMeasureSpec
和heightMeasureSpec
是几个意思?是用来测量自身大小的宽高么? - measure方法是
final
修饰的,既然是final的,就是不允许子类重写该方法,那怎么通过重写此方法来实现自定义控件的测量方式呢?
要回答第1个问题,首先得弄清楚:在界面的绘制过程中,View的这个方法是被它的父控件调用的,也就是说widthMeasureSpec和heightMeasureSpec是通过父控件传递进来的,如果这两个参数是完全用来决定孩子View的大小,那孩子们也太没主动权了,凭什么你让我长多大我就多大,是我老子也不行。
装个逼但是事实上,这两个参数确实在很大程度上是决定了一个View的尺寸的,只不过孩子View可能各有各的特点,它们是能根据自身的特点来进行调整的,具体的呢以后再说。widthMeasureSpec和heightMeasureSpec都是32位的int类型的数,这么特殊肯定是有自己的故事滴,它的高2位代表测量模式Mode
,低30位代表测量大小Size
。系统提供了一个MeasureSpec类来对这个参数进行操作,先来具体的看看MeasureSpec
(测量规格):
测量规格MeasureSpec
首先,贴出MeasureSpec的源码:
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;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
上面的代码不是很复杂,都是通过位运算来进行操作的。(我在平时位运算用的少,所以我还得慢慢捋一捋才看的明白。╥﹏╥...)不过,这样做的好处就是更省内存,因为要是我来做的话,肯定是为这样的测量规格定义一个类,里面有mode和size两个属性,这样每次就会new很多测量规格的对象了。
好了,喝口水,接着往下说。既然测量规格是由测量模式mode和测量大小size组成的,size好说,那测量模式mode代表什么含义呢?源码写的很清楚,测量模式有三种:
-
UNSPECIFIED
父控件不对你做任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大) -
AT_MOST
跟上面差不多,也是你想要多大就可以给你多大,但是要满足一点要求,你的大小不能大于父控件给你指定的size哦,不然可抱不动你咯,但具体是多少,还是要看你自己的实现。 -
EXACTLY
父控件已经知道你想要多大了,那你就这么大吧。满足你的要求!
相信上面三种模式的区别我们都弄清楚了吧,但是父控件是怎样给它的孩子们构建好测量大小和测量模式的呢?这其中必有蹊跷。好吧,冤有头债有主,我们得去ViewGroup类里去找找看。ViewGroup里提供了一个静态方法getChildMeasureSpec
用来获取子控件的测量规格,下面是代码和详细注释:
/**
* 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
* 最可能符合条件的child view的测量规格。
* @param spec 父控件的测量规格
* @param padding 父控件里已经占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return 返回child view 的测量规格
*/
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:
//如果child的布局参数有固定值,比如"layout_width" = "100dp"
//那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局参数是"match_parent",也就是想要占满父控件
//而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
//比如TextView根据设置的字符串大小来决定自己的大小
//那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
//所以测量模式就是AT_MOST,测量大小就是父控件的size
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
case MeasureSpec.AT_MOST:
//同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
//但同样的,child的尺寸上限也是父控件的尺寸上限size
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
//child想要根据自己逻辑决定大小,那就自己决定呗
else if (childDimension == LayoutParams.WRAP_CONTENT) {
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
根据上面的代码,可以列出默认情况下,View的测量规格的生成规则:
测量规格的生成规则现在我们知道了,View的测量规格是由父控件的测量规格和自身的LayoutParams
共同决定的。并且在普通情况下,会满足上面表格里的规则。但是那是在普通情况下,而在我们自定义控件中,有时候是根据特有的逻辑去得到测量规格的。所以,掌握好原理,以不变应万变才是上策。ok, 第一个问题完美解决。
解释完测量规格MeasureSpec,就让我们回到一开始提出的第2个问题:
- measure方法是
final
修饰的,既然是final的,就是不允许子类重写该方法,那怎么通过重写此方法来实现自定义控件的测量方式呢?
现在我们来看一下measure的具体实现:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = forceLayout ? -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("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
...
}
看到了我们熟悉的onMeasure
方法啦,虽然measure方法不能被重写,但是onMeaure方法是可以被重写的。所以我们想要实现自己自定义控件的测量方式,就得重写onMeasure方法。再来跟进看看onMeasure方法的实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这方法一层嵌一层的,还是从里往外接着看吧,对于getSuggestedMinimumWidth
和getSuggestedMinimumHeight
方法,顾名思义,就是得到建议的最小的宽/高。什么意思呢?以getSuggestedMinimumWidth为例:
protected int getSuggestedMinimumWidth() {
//如果有设置背景,则获取背景的宽度,如果没有设置背景,则取xml中android:minWidth的值。
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
mMinWidth
属性对应的就是xml布局里的android:minWidth
属性,设置最小宽度。mBackground.getMinimumWidth()
方法返回的就是View背景Drawable的原始宽度,这个宽度跟背景的类型有关。比如我们给View的背景设置一张图片,那这个方法返回的宽度就是图片的宽度,而如果我们给View背景设置的是颜色,那么这个方法返回的宽度则是0。具体的大家可以自行查阅Drawable尺寸的相关资料。所以,这个方法的返回的宽度是:如果View没有设置背景,那就返回xml布局里的android:minWidth属性定义的值,默认为0;如果View设置了背景,就返回背景的宽度和mMinWidth中的最大值。
再来看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;//这里的size就是上面getSuggestedMinimumWidth/height的返回值
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
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在xml布局中使用wrap_content
,根据上面提到的规则表格,它的测量模式是AT_MOST模式,测量尺寸specSize是parentSize,而getDefaultSize方法在AT_MOST里直接返回specSize,也就是等于父容器的剩余空间大小,这和match_parent是一样的。所以我们需要自己来处理AT_MOST模式下的宽高。大多数情况下也是这样的。
下面举个例子:一个重写onMeasure方法来支持wrap_content属性的模版如下:
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){
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}
以上代码可以直接应用到我们的自定义控件里去,当然最重要的还是大家得对AT_MOST模式留点心,记得对它特别对待就行。
最后,我们再看最外层的方法setMeasuredDimension
:
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);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasuredDimension方法里调用了setMeasuredDimensionRaw
方法,在这个方法里面,终于看到了我们熟悉的mMeasuredWidth和measuredHeight的赋值语句。经过这一步,我们就可以安心的调用View的getMeasureWidth()和getMeasureHeight()方法了!(≧∇≦)ノ View的测量过程到这里就完成了。
2.2 ViewGroup的测量过程
ViewGroup并没有重写View的onMeasure方法,这需要它的子类去根据相应的逻辑去实现,比如LinearLayout与RelativeLayout对child view的测量逻辑显然是不同的。不过,ViewGroup倒是提供了一个measureChildren
的方法,貌似可以用来测量child的样子,看看源码:
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
方法对其进行测量,接着来跟进看看measureChild:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 取出孩子的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方法的逻辑在上面的MeasureSpec部分有详细说明。最后,当然还是得调用child的measure方法啦,让孩子根据父母的指引去测量自己。
在我看来,我们在自己自定义控件时,上面的这两个方法几乎不会用到。因为measureChildren太过简单粗暴,我们一般都会考虑孩子们之间的逻辑关系(顺序、间隔等),再计算他们的测量规格。不过这个方法也给我们一点启示,就是:
测量子元素时,对可见性为GONE的View要做特殊处理,一般来说就是跳过对它们的测量,来优化布局。
不知道大家有没有考虑过,measureChild方法只考虑了父控件的padding,但是没考虑到child view的margin,这就会导致child view在使用match_parent
属性的时候,margin属性会有问题。(什么?你说你自定义的ViewGroup对孩子不支持margin属性不就不会有问题了么...是是是,那当我没说....)当然,ViewGroup里为此也提供了另一个测量child的方法:
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考虑了进去,所以一般情况下,这个方法使用的更多一些。
3.布局流程-layout
布局的流程就没有测量流程那么“蜿蜒曲折”了。对于单身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;
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);
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
...
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
return changed;
}
从上面的代码,能看到layout方法首先会调用setFrame
方法来给View的四个顶点属性赋值,即mLeft
,mRight
,mTop
,mBottom
四个值,此时这个View的位置就确定了。同时我们也就能通过调用getWidth()
和getHeight()
方法来获取View的实际宽高了。
接下来,onLayout
方法才会被调用,这也意味着我们在自定义ViewGroup时,想要重写onLayout方法给我们的子元素定位,是可以直接调用getWidth()和getHeight()方法来获取ViewGroup的真实宽高的(setFrame方法在onLayout方法之前执行)。在View类里的onLayout方法是个空方法,而在ViewGroup方法里声明成了抽象方法,所以继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。
最后,在layout方法的最后我们能看到一个OnLayoutChangeListener
的集合
,看名字我们也猜得出,这是View位置发生改变时的回调接口。所以我们可以通过addOnLayoutChangeListener
方法可以监听一个View的位置变化,并做出想要的响应。(看源码的时候才发现这个回调接口的,以前都不知道。新技能get!︿( ̄︶ ̄)︿)
4.绘制流程-draw
绘制的流程也就是通过调用View的draw
方法实现的。draw方法里的逻辑看起来更清晰。一般是遵循下面几个步骤:
1.绘制背景 -- drawBackground()
2.绘制自己 -- onDraw()
3.绘制孩子 -- dispatchDraw()
4.绘制装饰 -- onDrawScrollbars()
Step 1 :绘制背景 -- drawBackground()
对View的背景进行绘制,这里调用了View的drawBackground
方法,我们看它的源码:
private void drawBackground(Canvas canvas) {
//mBackground是该View的背景参数,比如背景颜色
final Drawable background = mBackground;
if (background == null) {
return;
}
//根据View四个布局参数来确定背景的边界
setBackgroundBounds();
...
//获取当前View的mScrollX和mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
//如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
可以看出,这里考虑到了view的偏移参数,scrollX和scrollY,绘制背景在偏移后的view中绘制。
Step 2 :绘制自己 -- onDraw()
由于不同的控件都有自己不同的绘制实现,所以View的onDraw
方法肯定是空方法。这里就不贴源码了,大家在自定义控件时,可以根据实际需求对onDraw方法进行实现。
Step 3 :绘制孩子 -- dispatchDraw()
如果当前的View是一个ViewGroup类型,那么就需要绘制它的子View,这里调用了dispatchDraw
,而View中该方法是空实现,实际是ViewGroup重写了这个方法,那么我们来看看:
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
ViewGroup里的dispatchDraw方法遍历调用drawChild
方法,drawChild方法又调用了child的draw(canvas, this, drawingTime)
方法,最后还是调用到了child的draw(canvas)
方法。如此这般,绘制流程也就一层一层的传递下去了。
到此,View的绘制流程总算是说完了...(*゚ー゚).........................................
5.总结
View的绘制流程和事件分发机制是Android中两个重要的主题,上一篇《必知必会 | Android事件分发机制及滑动冲突解决方案》已经把事件的分发机制做了总结。这一篇是针对View的绘制流程,View的绘制基本分为measure、layout、draw 过程,其中比较复杂就是measure过程,所以本篇文章大幅笔地分析measure过程,相对讲得比较详细,如果你对View的绘制还不是很懂,对measure过程掌握得不是很深刻,那么耐心点,多看几遍,相信你会有所收获!
若文中有叙述不清晰或是不准确的地方,希望大家能够指出,谢谢大家!
View的绘制流程属于自定义控件的基本功,还需要在实践中多积累一些相关的经验,并逐渐做到融会贯通,这样才能提高自己的水平。keep going!