参考资料《Android开发艺术探索》
初识ViewRoot和DecorView
ViewRoot对应与ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure,layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上,针对performTraversals的大致流程如下
如图所示,performTraversals会以此调用performMeasure,performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure,layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了这个View树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有本质区别。
measure过程决定了view的宽/高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高,但是特殊情况除外,这点之后会说明。Layout过程决定了View的四个顶点的坐标和实际的View的宽/高,完成以后,可以通过getTop,getBottom,getLeft,getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到View最终的宽/高。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。
DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两部分(具体情况和android版本及主题有关),上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,因此可以理解为Activity指定布局的方法不叫setView而叫setContentView,因为我们的布局的确加到了id为content的FrameLayout中。
理解MeasureSpec
MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说很大程度是因为这个过程还受父容器的影响,父容器影响View的MeasureSpec的创建过程
MeasureSpec
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。下面看一下MeasureSpec内部的一些常量的定义,通过下面的代码,应该不难理解MeasureSpec的工作原理
MeasureSpec通过SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包为一个MeasureSpec,而一个MeasureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。
SpecMode有三类,每一类都表示特殊的含义,如下所示:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应与LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST
父容器指定了一个可用大小及SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应与LayoutParams中的wrap_content。
MeasureSpec和LayoutParams的对应关系
上面提到,系统内部是通过MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。另外,对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同确定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码,它展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸。
DecorView的MeasureSpec的产生过程,具体来说是遵守如下规则,根据它的LayoutParams中的宽/高的参数来划分
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小
LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
固定大小(比如100dp):精确模式,大小为LayoutParams中的指定大小
对于普通view来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,而ViewGroup的measureChildWithMargins方法,会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。而子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin即padding有关。
View的工作流程
View的工作流程主要是指measure,layout,draw这三大流程,即测量,布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个定点的位置,而draw则将View绘制到屏幕上。
measure过程
measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,出了完成自身#的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。
View的measure过程
View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可,View的onMeasure方法如下所示。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上述代码很简洁,到那时简洁不代表简单,setMeasuredDimension方法会设置View宽/高的测量值,因此我们只需要看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;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看出,getDefaultSize这个方法的逻辑很简单,对于我们来说,这里指需要讨论AT_MOST和EXACTLY这两种情况。简单的理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到的测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View的测量大小跟最终大小是相等的。
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小位getDefaultSize的第一个参数size,即宽/高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值,看一下他们的源代码
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
这里只分析getSuggestedMinimumWidth方法的实现,getSuggestedMinimumHeight和它的实现原理是一样的。从getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,那么View的宽度位mMinWidth,而mMinWidth对应与android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值。这个属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth())。mMinWidth的含义已经知道了,那么mBackground.getMinimumWidth()是什么呢?我们看一下Drawable的getMinimumWidth方法,如下
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。那么Drawable在什么情况下有原始宽度呢?举个例子,ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸)。
getSuggestedMinimumWidth的逻辑就是,如果View没有设置背景,那么返回android:minWidth这个属性所设置的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽/高。
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还回遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫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);
}
}
}
从上述代码看,ViewGroup在measure时,会对每一个子元素进行measure,measureChildren这个方法的实现也很好理解,如下所示
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);
}
很显然,measureChild的思想就是取出子元素的LayoutParams,然后在通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。
我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,这是因为不同的ViewGroup具有不同的布局特性,这导致他们的测量细节各不相同
layout过程
layout过程的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,而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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点得到位置,即初始化mLeft,mTop,mBottom,mRight这四个值,View的四个顶点一旦确认,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正的实现onLayout方法。接下来,我们可以看一下LinearLayout的onLayout方法,如下
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的实现逻辑和onMeasure的实现逻辑类似,这里选择layoutVertical继续讲解,
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
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
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;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
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) {
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;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
分析一下代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合LinearLayout的特性。至于setChildFrame,它仅仅是调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层的传递下去就完成了整个View树的layout过程,setChildFrame方法实现如下所示
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
我们注意到,setChildFrame中的width和height实际上就是子元素的测量宽/高,从下面代码可以看出
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
而在layout方法中会通过setFrame去设置子元素的四个顶点的位置,在setFrame中有如下几句赋值语句,这样一来子元素的位置就确定了
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
View的测量宽/高和最终的宽/高有什么区别?这个问题可以具体为:view的getMeasureWidth和getWidth这两个方法有什么区别?看一下getWidth这个方法的具体实现
public final int getWidth() {
return mRight - mLeft;
}
从getWidth的源码在结合mLeft,mRight,mTop和mBottom这四个变量赋值来看,getWidth方法返回的值刚好就是View的测量宽度。经过上述分析,可以回答:在view的默认实现中,view的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于view的measure过程,而最终宽/高形成与layout过程,即两者的赋值时机不同,测量宽/高的赋值时机早一些。因此,在日常开发中,我们可以认为view的测量宽/高就等于最终宽/高,但是某些特殊情况会导致两者不一致,好比重写layout方法
public void layout(int l, int t, int r, int b) {
super.layout(l,t,r+100,b+100);
}
上述代码会导致在任何情况下view的最终宽/高总是比测量宽/高大100px,这样做虽说没什么意义,但是可以证明测量宽/高的确可以不等于最终宽/高。另外一种情况是在某些情况下,view需要多次measure才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。
draw过程
draw过程就比较简单了,它的作用是将view绘制到屏幕上,view的绘制过程遵循如下几步
(1)绘制背景 background.draw(canvas);
(2)绘制自己 (onDraw);
(3)绘制children (dispatchDraw);
(4)绘制装饰 (onDrawScrollBars);
这一点通过draw方法的源码可以看出来,如下
public void draw(Canvas canvas){
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
/*
* 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);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有的子元素的draw方法,如此draw事件就一层一层的传递了下去。view有一个特殊方法setWillNotDraw,先看一下源码
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从setWillNotDraw这个方法的注释中可以看出,如果一个view不需要绘制任何内容,那么设置这个标记为true以后,系统会进行相应的优化。默认情况下,view没有启用这个优化标记,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于ViewGroup并且自身不具备绘制功能时,就可以开起这个标记为从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。
自定义View
自定义View的分类
自定义View的分类标准不唯一,下面分成四类
1.继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
2.继承ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,即除了LinearLayout,RelativeLayout,FrameLayout这几种系统布局之外,我们重新定义一种布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方式来实现。采用这种方式稍微复杂一些,需要合适的处理ViewGroup的测量,布局这两个过程,并同时处理子元素的测量和布局过程。
3.继承特定的View(比如TextView)
这种方式比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding等。
4.继承特定的ViewGroup(比如LinearLayout)
这种方式也比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方式来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和第二个方法的区别,一般来说2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
自定义View须知
1.让View支持wrap_content
这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。
2.如果有必要,让你的View支持padding
这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起到作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
3.尽量不要在View中使用Handler,没必要
这是以为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确的要使用Handler来发送消息
4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
这一条也很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
5.View带有滑动嵌套的情形时,需要处理好滑动冲突
如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将会严重影响View的效果。