每个人对于Android中的自定义View的测量、布局及绘制原理,都有自己独特的分析与看法,但终究的理论都是一样的。
- 自定义控件的分类
[1]通过系统提供的原生控件进行组合 来达到自定义的需求
[2]
- 定义一个类继承View
- 定义一个类继承ViewGroup
[3]View&ViewGroup特点
- View 是构建页面的基本模块 view类在屏幕上占据的是一个矩形区域 职责:是绘制和事件的处理
- ViewGroup:可以包裹孩子
View的绘制过程就是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程才能最终将一个View绘制出来:
- measure用来测量View的宽和高。
- layout用来确定View在父容器中放置的位置。
- draw用来将view绘制在屏幕上。
- 那么,对于ViewRoot是什么呢?
初始ViewRoot 与 DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的,在ActivityThread中,当ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
那也许会问DecorView又是什么呢?
其实它就是Activity的顶级View
在Activity中加载布局使用的方法中setContentView,那个contentview就是我们这个DecroView中的子布局,View的点击事件都要先经过DecorView,然后再传递给我们的View。
一、View绘制的流程框架
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout、draw这个三个流程。也就是说View的绘制是从上往下一层层迭代下来的。
其中:**1.perfromMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父元素的measure过程,如此反复就完成整个View树的遍历。
2.performLayout的传递流程和performMeasure是一样的。
3.performDraw的传递过程是在draw方法中通过dispathDraw来实现的,本质上并没有区别。
二、Measure流程
测量每一个控件的大小。
调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}
measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
Measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等于View的最终宽高,这仅仅是在代码规范的前提之下。
由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?
如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。
MeasureSpec的理解:
MeasureSpec的中文意思是测量规格的意思,MeasureSpec代表一个32的int值,高2位代表SpecMode测量模式,低30位代表SpecSize测量规格大小。
经常使用的三个函数:
1.public static int makeMeasureSpec(int size,int mode)
构造一个MeasureSpec
2.public static int getMode(int measureSpec)
获取MeasureSpec的测量模式
3.public static int getSize(int measureSpec)
获取MeasureSpec的测量大小
由图可知其中MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。
Mode的模式又分为三类
UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部
EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
那么MeasureSpec又是如何确定的?
对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。
这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。
对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。
MeasureSpec和LayoutParams的关系
在上面系统中,使用MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置layoutparams,在View测量的时候,系统会将LayoutParams在父容器的约束下自动转化成对应的MeasureSpec,然后再根据MeasureSpec来确定View最终的宽高。
MeasureSpec不是由LayoutParams唯一决定的,子View的宽高由自身的layoutparams和父容器的MeasureSpec。
对于DecorView,其MeasureSpec由窗口的尺寸和自身的Layoutparams决定;
对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;
遵循的规则:
1.LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
2.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
3.固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
**4.当view采用固定的宽高时,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams的大小
5.当View的宽高时matchparent时,如果父容器是精确模式,那么View也是精确模式,并且View也是精确模式并且大小是父容器的剩余空间。如果父容器是最大模式,那么View也是最大模式但是大小不能超过剩余的空间。
6.当View是wrap_content,那么不管父容器的模式,View一定是最大模式,但是不能超过父容器的剩余空间。**
总结:当子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和设置成match_parent时,子View的大小没有区别。为了显示区别,一般在自定义View时,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。
从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。
三、View的工作流程
View的工作流程主要是指measure、layout、draw这三个流程,即测量、布局、绘制,其中measure确定View的测量宽高,layout确定View的最终宽高和上下左右的位置,draw将View绘制到屏幕上。
清晰View测量流程
Measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等于View的最终宽高,这仅仅是在代码规范的前提之下。
layout最终决定了View的四个顶点的坐标和实际View的宽/高,完成以后,可以通过getTop、getBottom、getLeft、getRight来拿到View的四个顶点坐标位置,并可以通过getWidth和getHeight来得到View的最终宽高
draw过程决定了View的显示,只有draw方法完成以后View的内容才会最终显示在屏幕上。
①measure
measure过程有时分两种情况
- 针对View的measure
- 针对ViewGroup的measure
1.针对View的measure
View的measure过程由其meaure方法完成,measure方法是一个final类型的方法,子类不可以重写此方法,因为View的measure内部会调用onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
里面就是将getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)获取到的宽和高赋给View。
再看一下getDefalutSize的代码实现
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;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
其中,如果当前的模式为测量模式为UnSpecified,那么View的宽高就是传递过来Size的大小,如果测量模式为AT_MOST或者EXACTLY,那么View的宽高就是SpecSize的大小。也就是说View的内部将matchparent和wrapcontent是一样处理的,没有区别,所以这就是我们自定义View时,要自己处理wrapcontent的原因。
结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。matchparent使用的是父View剩余的控件。
解决方法:重写View的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 你自己计算出来的宽;
int measureHeight = 你自己计算出来的高;
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightSpaceMode == MeasureSpec.AT_MOST && widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measuredWidth, measureHeight);
} else if (heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpaceSize, measureHeight);
} else if (widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
所以只需要判断measureSpec的mode是不是AT_MOST,如果是就我们自己计算好的值赋给View。
2.针对ViewGroup的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);
}
}
}
可以看出measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量。
总结:
1.View的onmeasure在measure中,onmeasure中处理wrap_content和padding。
2.ViewGroup的onmeasure在measure中,viewgroup除了要测量自身的宽高,还要通过measureChild测量子View的宽高,只有里面子View的宽高确定了,才好计算Viewgroup的宽高。
3.注意MeasureSpec和layoutParams的对应关系。
②Layout
layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用Layout方法,在layout方法中onLayout方法又会被调用。这样一层一层的完成所有子View的布局过程。
layout里面通过setFrame来确定View的四个顶点的位置,然后再layout中调用onLayout,这个方法用来确定子View的位置,起到一层层传递的作用。
public void layout(int l, int t, int r, int b) {
// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// setFrame() / setOpticalFrame():确定View自身的位置
// 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果视图的大小和位置发生变化,会调用onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// onLayout():确定该View所有的子View在父容器的位置
onLayout(changed, l, t, r, b);
...
}
③draw操作
draw的过程也比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循下面:
1.绘制背景 (drawBackground)
2.绘制自己 (onDraw(canvas))
3.绘制children ( dispatchDraw(canvas))
4.绘制装饰 onDrawForeground(canvas);
@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);
}
无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
四、总结
从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都需要实现该方法,因其是个空方法
嗯,对于自定义View的绘制原理都透彻明了了,相信对大家回头看的时候还有些看得见的理论可去弥补。