自定义View基础之——初识View

界面永远离不开各种各样的控件,而这些控件,无论是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.
简单翻译下就是,这两个参数是由父view强加给子view的水平或者垂直空间要求。也就是说并不只是通过xml中的layout_width或layout_height来决定咯?其实对于普通的view,它的MeasureSpec都是由父容器自身的MeasureSpec和view自身的LayoutParams(也就是layout_width和layout_height属性)共同决定的。而父容器的MeasureSpec则由它的父容器的MeasureSpec和它自身的LayoutParams共同决定,继续向上追溯到顶级View(DecorView),则是由窗口的尺寸和其自身的LayoutParams来共同决定的。

了解了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度鞠躬!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值