原文:http://blog.csdn.net/guolin_blog/article/details/17045157
自定义View一直是一个很头疼的东西,写过了好几个控件,但是对这个还是知之甚少。看完了郭大神的博客后,决定把学到的记录下来,写这篇博客主要是让自己多提升一些,多知道一些总没一些坏处吧。
每一个自定义View总是经过了三个流程,OnMeasure(),OnLayout()和OnDraw()。下面我来对这三个阶段说一下我的理解。
一.OnMeasure()
measure 测量的意思,顾名思义这个方法是用来测量控件的大小的。一个View的绘制可以从ViewRoot的performTraversals()方法开始,在其内部调用View的measure()方法
那我们先来看一下View的measure()方法吧!
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
}
onMeasure(widthMeasureSpec, heightMeasureSpec);
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
这个方法是final类型,说明他不允许子类去重写这个方法
这个方法接受两个参数,一个参数是widthMeasureSpec还有一个是heightMeasureSpec,嗯,对,一个宽,一个高,这个MeasureSpec其实就指定了大小和规格,其实SpecSize就记录了大小,SpecMode记录了规格,我们来看一下SpecMode有哪些类型
注:Spec为specification的缩写,以为规格或者说明书的意思(英语不好,专门 用英语翻译软件翻译了一下)。
1.EXACTLY
这个表示父视图希望子视图通过SpecSize来指定视图大小,在XML中就是match_parent的意思。
2.AT_MOST
从字面意思,父视图希望子视图的最大大小不要超过SpecSize,应该尽可能小的去设置这个值,在XML中就是wrap_content的意思。
3.UNSPECIFIED
表示可以设置成任何大小,没有限制,这个我也没怎么用过,好像在ScrollView中会用到。
注:这个只是父视图给子视图的建议,你也可以自由设置
然后接着看这段代码的第9行,调用了OnMeasure()方法,这个才是真正设置控件大小的地方,这个方法会默认调用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;
}
这个measureSpec就是OnMeasure传入的值,如果是UNSPECIFIED,就会返回传入的result,如果是EXACTLY或者AT_MOST就会返回meassure得到的specSize,之后就会调用setMesuredDimension来设置控件大小。
不过控件的measure方法的值是从哪来的呢,没错是从父容器中得来的,那我们来看看这是个什么过程
ViewGroup定义了一个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():
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的时候OnMeasure()是可以重写的。
总结一下这个流程:父容器给子视图一个建议(XML设置的值,父容器的大小),子视图可以根据这个建议来设置大小
二.onLayout()
设置好了控件的大小,那放在哪就是一个问题,onLayout()方法就是这个用处,让我们来看一下layout()方法吧:
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
}
首先setFrame判断视图是否改变过,如果改变了,就调用onLayout()方法,然后我们点进去会发现这个竟然是一个抽象方法,所以你每自定义一个ViewGroup的时候都要重写这个方法,因为这个方法很关键,不然控件放哪呢?
下面是一段简单的实例:
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
System.out.println("onLayout");
if (getChildCount()>0) {
View childView=getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
这样能把父容器的第一个View设置好了位置
三.onDraw()
这个方法就是在定义的View上大显身手了
看一下draw()方法
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
可以看到,第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。
接下来的第三步是在第34行执行的,这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。
第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。
以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。
下面是一个小例子:
protected void onDraw(Canvas canvas) {
Paint paint=new Paint();
paint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, 200, 200, paint);
paint.setColor(Color.BLUE);
paint.setTextSize(20);
canvas.drawText("Hello View", 0, 20, paint);
System.out.println("CustomView Draw");
}
canvas就像一个画布,你可以在上面画画,Paint就像一个画笔,然后你就可以开始大显身手了!
好了,今天收获那么多,记录下来,以后可以回来复习复习。