界面永远离不开各种各样的控件,而这些控件,无论是TextView,Button,ImageView,甚至ListView等等,他们都有一个共同的基类,那就是View。但是,哪怕有了如此多的控件,有时候依旧满足不了我们设计师的胃口,时不时会冒出各种各样酷炫吊炸天的界面,这时候就需要我们自己去自定义View了。例如说,绘制一个圆形头像,绘制图片的加载进度条,或者实现上拉刷新下拉加载的操作等等,这些都是通过自定义View的实现。想要自定义View,那么首先就要先了解View:
一、位置,尺寸:
对于Android系统中的每一个View都会在界面中占据一块矩形的区域,自然也就包括left,top,right,bottom四个属性,我们可以使用相应的get方法进行获取,具体几个方法如下:
getLeft():获取view的left边相对于父view的距离,左上角的横坐标。
getTop():获取view的top边相对于父View的距离,左上角的纵坐标。
getRight():获取view的right边相对于父View的距离,右下角的横坐标。
getBottom():获取view的bottom边相对于父View的距离,右下角的纵坐标。
而view的尺寸是以宽度和高度来表达的,事实上一个view拥有两组宽和高的值。一组是measured width和measured height,可以使用getMeasuredWidth()和getMeasuredHeight()来获取,这组尺寸指的是view想要在父布局内是多大。第二组尺寸是width和height,这组尺寸定义了view在屏幕上绘制时候的实际尺寸,可以使用getWidth()和getHeight()方法获取,两组尺寸大多数情况下一样。两组尺寸大多数情况下一样,那么时候不一样呢?等到接下来再说。为了测量尺寸,view通常需要将padding也要考虑进去,如果有必要的话,其实在自定义view的onDraw()方法里也应该处理padding,不然padding是无法起到任何作用的。而margin则是只有我们自定义ViewGroup的时候才会去考虑。
二、view的绘制过程:
view的绘制流程依次是measure过程,layout过程和draw过程。其实我们稍微一想也就知道这个大概思路了,得首先进行measure测量过程,知道了view的宽度和高度;之后layout布局过程,由父布局安排view的位置;最后进行draw过程,将view绘制到屏幕上。
1、measure过程
view的measure是通过调用measure()这个方法来实现的,当测量过程结束,measure()方法返回之后,就可以获取measuredWidth和measuredHeight了,通过getMeasuredWidth()和getMeasuredHeight()来获取。
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
而measure()方法,很明显是个final方法,这代表子类不能重写这个方法,而在measure()方法内部,则会去调用onMeasure()方法,确切的测量工作也都是在onMeasure()这个方法里执行的。而我们通常自定义View的时候,需要重写的也就是onMeasure()方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我们在onMeasure()方法中可以看到widthMeasureSpec和heightMeasureSpec这两个参数,也是measure()方法传递进来的。这里就不得不提MeasureSpec,虽然已经有很多博客仔细研究过,可能你们都厌烦了,可是它确实不可或缺,我还是要在这里好好说一遍。MeasureSpec是一个32的int值,高2位代表的SpecMode,低30位代表的是SpecSize。SpecMode有以下三种:
EXACTLY:父容器已经知道view需要的确切大小,就是SpecSize。通常我们将layout_width或layout_height指定为具体数值,或者指定为match_parent的时候,对应的就是这种模式。
AT_MOST:父容器给定了最大值SpecSize,view的大小不能超过这个值。通常我将layout_width活layout_height指定为wrap_content的时候,对应这种模式。
UNSPECIFIED:把它放在最后不是因为它最重要,而是因为它用的比较少。这是指父容器不对view进行任何限制,view想多大就多大。
通常我们在自定义view重写onMeasure()方法的时候,通过widthMeasuSpec和heightMeasureSpec就可以获得相应的SpecMode和SpecSize:
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
当然,在重写onMeasure()方法的时候,最后一定要调用setMeasuredDimension(widthMeasureSpec,heightMeasureSpec)。
有人可能要问了,为什么要重写onMeasure()方法?那是因为view类默认的onMeasure()方法只支持EXACTLY模式,具体原因接下来说。想象一下,如果你自定义了一个view,然后在xml中设置android:layout_width="wrap_content",运行起来却发现你的自定义view铺满了全屏,很明显这不是你想要的结果,那是多糟糕的体验啊,这时候你想要支持wrap_content就必须重写onMeasure()方法了。
继续回到我们之前的话题,onMeasure()方法中的两个参数widthMeasureSpec和heightMeasureSpec,我们已经知道了它们是MeasureSpec类型,也知道了如何获取它们的SpecMode和SpecSize,那么它们是怎么来的呢?每次重写onMeasure()方法的时候,可能大家都在疑惑,这两个参数是靠什么决定的呢,它们只是通过我们在xml中设置layout_width或者layout_height就确定了吗?我们先看下官方注释:
widthMeasureSpec | horizontal space requirements as imposed by the parent. The requirements are encoded with View.MeasureSpec . |
---|---|
heightMeasureSpec | vertical space requirements as imposed by the parent. The requirements are encoded with View.MeasureSpec . |
了解了onMeasure()方法中的两个参数之后,我们继续来看方法内的具体内容,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;
}
代码很简单,就是根据提供的widthMeasureSpec或heightMeasureSpec来确定测量所得的width或height。代码中我们可以看到,无论是AT_MOST还是EXACTLY,最终的所得到的测量的大小都是specSize,也就是我们提供的参数measureSpec中得来的。也就是说哪怕我们设置了wrap_content,最终我们得到的宽或高并不是wrap_content,而是match_parent的父容器允许的最大值。这样也就解答了“view类默认的onMeasure()方法只支持EXACTLY模式”这个问题,我们想要支持wrap_content,只能重写onMeasure()方法。
以上说的都是单独view的测量过程,而对于ViewGroup来说,measure过程是一个自顶向下的树的遍历,除了执行自己的测量过程外,还会去执行所有子元素的measure()方法,各个子元素再递归去执行这个流程。
2、layout过程
layout过程,作为整个绘制流程的第二阶段,父容器会根据在measure过程中获得的宽度和高度来安排所有子元素的位置,也是自顶向下的树的遍历。layout过程通过调用layout()方法来实现的,与measure()方法一样,我们在自定义ViewGroup的时候并不需要重写这个方法,而是重写onLayout()方法来确定子元素的位置。
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;
}
layout()方法是用来确定view本身的位置,其中会调用onLayout()方法,onLayout()方法则是用来确定所有子元素的位置的。
大致的流程就是,父元素在layout()方法中完成自己的定位,然后调用onLayout()方法,其中onLayout()方法中会继续调用子元素的layout()方法,子元素就可以确定自己的位置。这样一层层传递下去,就完成了整个view树的layout过程。
当我们自定义ViewGroup重写onLayout()这个方法的时候,需要注意的就是调用子view的layout()的时候,需要将margin考虑进去,自定义view并不需要重写onLayout()方法。
3、draw过程
费了这多事,我们终于来到了draw过程。作为整个绘制流程的最后一个阶段,当然也是最重要的部分,它的作用,就是将view绘制到屏幕上面。我们自定义view的时候,只需要重写onDraw()方法即可,之后使用canvas和paint在手,我们还不是想干什么就干什么。
draw过程也是调用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);
// 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;
}
...
}
其实看注释也基本上明白了整个View的绘制过程,大概以下几步:
(1)绘制背景 drawBackground(canvas)
(2)绘制view的内容 onDraw(canvas)
(3)绘制子元素 dispatchDraw(canvas)
(4)绘制装饰 onDrawForeground(canvas)
view绘制过程的传递是通过dispatchDraw()方法来实现的。ViewGroup通常情况下不需要绘制,但是ViewGroup会调用diapatchDraw()方法来绘制其子View。dispatchDraw()方法会遍历调用所有子元素的draw()方法,这样绘制流程就一层层的传递下去了。所以我们通常自定义view的时候才重写onDraw()方法,自定义ViewGroup的时候大多不重写onDraw()方法。
这样,我们整个View的绘制流程都说完了,大家对于View应该也有一定的了解了,是不是觉得view也就这么回事,是不是信心满满啦,对于自定义View也跃跃欲试了?可是只了解这些还是不够的,我们下一篇博客介绍自定义View时使用的主要工具canvas和paint,以及自定义View时需要重写的方法,敬请关注!
PS:我靠,这篇博客写了我4个小时啊,4个小时啊,4个小时啊啊啊啊啊啊!!!!我发现了写文档太费劲了,一个字一个字的憋,比挤牙膏累多了,分明是便秘啊。还是直接写项目博客开心,只要把代码一贴,随便说几句话,哪怕满屏写上哈哈哈哈,也迅速结束战斗呀。写文档不是人干的活啊,我都写了四小时了,还不够您看个5分钟么,求点击啊!!!
PS:真的佩服那些写小说的,几百万几百万字的就码出来了,为我那些年看的盗版小说道歉,90度鞠躬!