摘要
自定义控件在工作中时常有所接触,但始终是只得其形,不得其神。最近抽空系统学习了下这方面的知识,准备用“Android自定义控件”作为一个博客系列来记录我的学习心得。系列文中不乏引用到Android SDK源码,以API 25为准。最后,如有雷同,没错,就是我去抄的。
Android自定义控件系列目录
Android自定义控件 —— 事件分发
Android自定义控件 —— 三大流程
View的三大流程分为:测量(measure)、布局(layout)、绘制(draw),下面就一一进行探索吧。
measure
布局绘画涉及两个流程:测量流程和布局流程,测量流程通过measure(int, int)
实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout(int, int, int, int)
来实现的,也是自顶向下的。在这个过程中,每个父容器ViewGroup负责通过计算好的尺寸放置它的子View。
前面讲过,onMeasure(int, int)
是用来测量当前控件大小的,给onLayout(boolean, int, int, int, int)
提供数值参考,需要特别注意的是,测量完成以后通过setMeasuredDimension(int,int)
设置给系统。
MeasureSpec
这是一个View的内部类,介绍之前,先来看看源码
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
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(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
...
}
作用
MeasureSpec是用来规范测量数值的。什么意思呢?MeasureSpec规定了,一个规范的测量数值(int型,32位)应该由mode和size两部分组成,即应该由一个32位的二进制数“mode(前两位) + size(后30位,即原始测量数值)”的形式组成。
mode分类
mode是用两位二进制表示的数,其分类如下
UPSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略子元素本身的大小
AT_MOST:子元素至多达到指定大小的值
它们对应的二进制值分别是
UNSPECIFIED = 00
EXACTLY = 01
AT_MOST = 10
应用
在XML布局时,通过android:layout_width
和android:layout_height
来设置控件的宽高值,宽高值分为三种类型:wrap_content、match_parent、具体值,三种值类型所对应的mode为
wrap_content ——> MeasureSpec.AT_MOST
match_parent ——> MeasureSpec.EXACTLY
具体值 ——> MeasureSpec.EXACTLY
举个例子,测量的宽度值为50,那么其所对应的MeasureSpec规范数值就应该为:01 000000000000000000000000110010,其中,前两位(01)代表mode位,50对应的二进制数为110010,不足30位补位即可。
MeasureSpec和LayoutParams
上面了解了MeasureSpec这个类,那么该类是怎么创建来的呢?根据什么创建来的呢?现在就来深入了解一下。
对于普通View,其对应的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams来共同决定的。
普通View的measure过程由ViewGroup传递而来,那么来看看源码了解下MeasureSpec是怎么被创建出来的
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
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);
}
...
}
上述方法,首先获取到了子元素的LayoutParams,然后根据当前子元素的LayoutParams和父容器的MeasureSpec来获取子元素的 MeasureSpec,最后调用了子元素的measure(int, int)
。
来看看getChildMeasureSpec(int, int, int)
的具体实现
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
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);
}
...
}
正如先前所说,上述方法主要是根据父容器的MeasureSpec同时结合子元素的LayoutParams来确定子元素的MeasureSpec。
由于UNSPECIFIED这个mode主要用于系统内部measure,一般来说,我们无须关注,所以排除掉UNSPECIFIED来总结一下就是
当子View采用固定宽高的时候,不管父容器的MeasureSpec是什么,子View的mode都是EXACTLY,并且其大小遵循LayoutParams中的大小
当子View的宽高设为match_parent的时候,如果父容器的mode为EXACTLY,那么子View的mode也是EXACTLY,并且大小是父容器的剩余空间。如果父容器的mode为AT_MOST,那么子View的mode也是AT_MOST,并且大小不会超过父容器的剩余空间
当子View的宽高设为wrap_content的时候,不管父容器的mode是EXACTLY还是AT_MOST,子View的mode都是AT_MOST,并且大小不能够超过父容器的剩余空间
View的measure流程
View的测量流程是由measure(int, int)
来完成的
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
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);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
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("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
}
...
}
发现该方法是一个final型方法,所以不能被重写。不过在其中回调了onMeasure(int, int)
,因此我们只需在View中重写onMeasure(int, int)
方法来完成View的测量即可。那么View默认的onMeasure(int, int)
实现是怎样的呢?
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
...
}
可以看到,该方法的实现很简单,直接调用了setMeasuredDimension(int, int)
来设置测量的尺寸。也就是说,关键就在于getDefaultSize(int, int)
方法里
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
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;
}
...
}
很显然,getDefaultSize(int, int)
方法返回的就是MeasureSpec的specSize(源码里的变量,即实际测量值)部分,而这个MeasureSpec是ViewGroup传递过来的。
到这里也就理解了,为什么当我们在布局中设置子View的宽高值为wrap_content的时候,如果不重写其onMeasure(int, int)
方法,则默认大小就是父容器的可用大小了。
当我们在布局中设置子View的宽高值为wrap_content时,那么测量模式(即mode)就是AT_MOST,在该模式下,它的宽高值等于specSize。而specSize由ViewGroup传递过来时就是parentSize(自定义的变量名称),也就是父控件的可用大小。
当我们在布局中设置子View的宽高值为match_parent时,那么不用多说,宽高值当然也是parentSize。这时候,我们只需对AT_MOST测量模式进行处理
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
if(widthMode == MeasureSpec.AT_MOST){
width = ...
}
if(heightMode == MeasureSpec.AT_MOST){
height = ...
}
setMeasuredDimension(widthMode != MeasureSpec.AT_MOST ? widthSize : width,
heightMode != MeasureSpec.AT_MOST? heightSize : height);
}
上述代码,判断当测量模式是AT_MOST时,自己计算View的宽高。其他情况,直接使用specSize。
至于UNSPECIFIED这种情况,则是使用的是getDefaultSize(int, int)
的第一个参数的值,也就是getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
所返回的值,一般用于系统内部的测量过程,这两个方法的源码如下
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
...
}
从源码看出,这是判断View有没有背景,没有背景的话,那么值就是View最小的宽度或高度,也就是对应XML布局中的android:minWidth
和android:minHeight
属性,如果属性没有指定的话,默认值为0;有背景的话,那么值就是View最小的宽度或高度和背景的最小宽度或高度,取两者中最大的一个值,这个值就是当测量模式是UNSPECIFIED时View的测量宽/高。
到这里就完成了整个View的measure过程,完成之后我们就可以通过getMeasuredWidth()
和getMeasuredHeight()
方法获取View正确的测量宽/高了。但是需要注意的是,在某些极端情况下,系统可能需要在多次measure过程后才能确定最终的测量宽/高,在这种情况下,直接在onMeasure(int, int)
中获取的测量宽/高可能是不准确的,保险的做法是在onLayout(boolean, int, int, int, int)
方法中去获取。
ViewGroup的measure流程
ViewGroup的measure流程和View不同,不仅需要完成自身的measure流程,还需要去遍历其所有子View,各个子元素再递归这个流程。ViewGroup提供了一个叫measureChildren(int, int)
的方法
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
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,判断如果子View没有GONE掉的时候,就继续执行measureChild(View, int, int)
方法
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
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的LayoutParams,然后通过getChildMeasureSpec(int, int, int)
创建子View的MeasureSpec,创建好后,将MeasureSpec传给子VIew进行其measure 流程。
解读源码发现,ViewGroup并没有定义其具体的测量流程(即实现onMeasure(int, int)
),这是因为ViewGroup是一个抽象类,它的测量流程需要其子类去实现不同的布局特性,没办法做统一实现,比如说像LinearLayout、RelativeLayout等。
layout
通过源码发现,layout流程的关键在于onLayout(boolean, int, int, int, int)
,实现View的layout流程,就是实现该View的布局,而如果换成是ViewGroup,那么就实现其所有子控件的布局。
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
...
}
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
...
}
draw
下面直接通过查看draw(Canvas)
的源码,来分析下其draw流程
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
public void draw(Canvas canvas) {
...
/*
* 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
...
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers
...
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
...
if (drawTop) {
...
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
...
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
...
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
...
canvas.drawRect(right - length, top, right, bottom, p);
}
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
...
}
上面的源码注释写的很清晰,通过查看后我们了解到View的绘制共分为如下六步
绘制背景
如果需要,保存图层信息
绘制View的内容
如果View有子View,绘制View的子View
如果需要,绘制View的边缘(如阴影等)
绘制View的装饰(如滚动条等)
其中以上六步,第二步和第五步并不是必须的,所以我们只需重点分析其他四步即可。
绘制背景
绘制背景调用了drawBackground(Canvas)
方法
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
private void drawBackground(Canvas canvas) {
// 获取背景 drawable
final Drawable background = mBackground;
if (background == null) {
return;
}
// 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
setBackgroundBounds();
...
// 获取 mScrollX 和 mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
// 如果 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
canvas.translate(scrollX, scrollY);
// 调用 Drawable 的 draw 方法绘制背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
...
}
绘制内容
绘制内容调用了View#onDraw(Canvas)
方法,由于View的内容各不相同,所以该方法是一个空实现,需要由子类去实现
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
protected void onDraw(Canvas canvas) {
}
...
}
绘制子View
绘制子View调用了View#dispatchDraw(Canvas)
方法,该方法同样是一个空实现
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
protected void dispatchDraw(Canvas canvas) {
}
...
}
当只有包含子View的时候,才会去重写它,ViewGroup不正好符合条件吗? 来看下ViewGroup对该方法的实现吧
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
protected void dispatchDraw(Canvas canvas) {
...
final int childrenCount = mChildrenCount;
...
for (int i = 0; i < childrenCount; i++) {
...
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
...
}
}
...
}
ViewGroup#dispatchDraw(Canvas)
方法的代码比较多,只分析重点,遍历了所有的子View并调用了ViewGroup#drawChild(Canvas, View, long)
方法
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
...
}
该方法最终还是调用了子View的draw(Canvas)
方法。
由于ViewGroup已经为我们实现了该方法,所以我们一般都不需要重写该方法。
绘制装饰
绘制装饰调用了View#onDrawForeground(Canvas)
方法,源码如下
package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
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);
}
}
...
}
该方法默认实现是绘制了滚动指示器、滚动条、和前景。
参考链接:
1. Android View 的绘制流程
2. 自定义控件三部曲视图篇(一)——测量与布局
3. Android 从0开始自定义控件之深入理解 MeasureSpec (六)
4. Android 从0开始自定义控件之 View 的 measure 过程(七)
5. Andriod 从0开始自定义控件之 View 的 layout 过程 (八)
6. Android 从0开始自定义控件之 View 的 draw 过程 (九)