Android基础之View的绘制

Android中的任何一个布局、控件都是直接或间接继承自View的,要想开发一个Android App,就肯定少不了要和View打交道。尽管Android已经提供了丰富的控件和布局,但是要想开发出有自己特色的App,自定义View是必须要掌握的。那么,今天我们就来聊聊View绘制的相关内容。
本文的要点如下:

  • View简介
  • MeasureSpecs类
  • onMeasure
    • 单一View的测量
    • ViewGroup的测量
  • onLayout
    • 单一View的layout过程
    • ViewGroup的layout过程
  • onDraw
  • 总结

View简介

在Android系统中View是所有控件的基类,其中也包括ViewGroup在内,ViewGroup是代表着控件的集合,其中可以包含多个View控件。
从某种角度上来讲Android中的控件可以分为两大类:View与ViewGroup。通过ViewGroup,整个界面的控件形成了一个树形结构,上层的控件要负责测量与绘制下层的控件,并传递交互事件。
在每棵控件树的顶部都存在着一个ViewParent对象,它是整棵控件树的核心所在,所有的交互管理事件都由它来统一调度和分配,从而对整个视图进行整体控制,如下图所示:



绘制出整个界面肯定是要遍历整个View树,对这棵树的所有节点分别进行测量,布局和绘制。万事皆有源头,绘制得从根节点顶级View开始画起,即DecorView。
系统内部会依次调用DecorView的measure,layout和draw三大流程方法。measure方法又会调用onMeasure方法对它所有的子元素进行测量,如此反复调用下去就能完成整个View树的遍历测量。同样的,layout和draw两个方法里也会调用相似的方法去对整个View树进行遍历布局和绘制。

View的构造函数:共有4个,具体如下:

    // 如果View是在Java代码里面new的,则调用第一个构造函数
 public CarsonView(Context context) {
        super(context);
    }

    // 如果View是在.xml里声明的,则调用第二个构造函数
    // 自定义属性是从AttributeSet参数传进来的
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

View的位置参数
View的位置由4个顶点决定(View的位置是相对于父控件而言的

  • Top:子View上边界到父view上边界的距离
  • Left:子View左边界到父view左边界的距离
  • Bottom:子View下边距到父View上边界的距离
  • Right:子View右边界到父view左边界的距离

MeasureSpecs类

MeasureSpecs类是View的内部类,用一个变量封装了两个数据(size、mode),其目的是减少对象的内存分配。

public static class MeasureSpec {
        //省略了部分不关键代码
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        // 通过Mode 和 Size 生成新的SpecMode
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        //获取测量模式(Mode)
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
            //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
            //例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST)
            //这样就得到了mode的值

        }
        //获取测量大小(Size)
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
            // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),
            //将32,31位替换成0也就是去掉mode,保留后30位的size  
        }
    }

测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)。
其中,测量模式占最高两位,测量大小则是MeasureSpec的低30位。测量模式(Mode)的类型有3种:UNSPECIFIEDEXACTLYAT_MOST。具体如下:

UNSPECIFIED:父View不约束子View(即子View可以获取任意尺寸),多用于系统内部View(ListView,ScrollView等),自定义View一般用不到。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
EXACTLY(精确模式):父View会为子View指定一个确切尺寸,子View必须在该尺寸之内。对应LayoutParams中的match_parent或具体数值。
AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。

onMeasure

View的绘制流程中,第一步就是测量,即onMeasure()方法。我们知道,自定义View的类型可以分为两种,一种继承View,一种继承ViewGroup(继承现有View的也可以根据继承的View的不同归入这两种类型之中)。那么测量的过程也有两种:

  1. 单一View的测量
  2. ViewGroup的测量
    那么下面我们就分别来看看两种流程的不同。

单一View的measure过程

这种情况相对而言比较简单,不用考虑子View,只有一个原始的View,通过measure()即可完成测量,具体过程如下:



那么我们来看看整个流程的源码:

//measure是测量流程的开始,由于是final类型,因此不能被重写,
//主要是用来进行基本测量逻辑的判断。
//里面调用onMeasure进行测量逻辑。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {           
            onMeasure(widthMeasureSpec, heightMeasureSpec);//具体的测量逻辑
        } else {
            ...    
    }

measure方法是测量过程中最先调用的方法,View的这个方法是被它的父控件调用的。由于measure是final类型,不能被子类重写,那么就只能重写onMeasure方法来实现测量逻辑了。

//在onMeasure中就做了两件事,
//1是根据View宽/高的测量规格用getDefaultSize()方法计算View的宽/高值,
//2是用setMeasuredDimension()方法存储测量后的View宽 / 高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}
protected int getSuggestedMinimumWidth() {
        //如果有设置背景,则获取背景的宽度,如果没有设置背景,则取xml中android:minWidth的值。
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

onMeasure里面用到了getSuggestedMinimumWidth和getSuggestedMinimumHeight,这两个方法差不多的,我们就以getSuggestedMinimumWidth为例。mMinWidth属性对应的就是xml布局里的android:minWidth属性,设置最小宽度。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始宽度,这个宽度跟背景的类型有关。比如我们给View的背景设置一张图片,那这个方法返回的宽度就是图片的宽度,而如果我们给View背景设置的是颜色,那么这个方法返回的宽度则是0。
所以,这个方法的返回的宽度是:如果View没有设置背景,那就返回xml布局里的android:minWidth属性定义的值,默认为0;如果View设置了背景,就返回背景的宽度和mMinWidth中的最大值。

//存储测量后的View宽 / 高,该方法即为我们重写onMeasure()所要实现的最终目的。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        //判断该view布局模式是否有一些特殊的边界
        boolean optical = isLayoutModeOptical(this);
        ////判断view和该view的父view的布局模式情况,如果两者不同步,则进行子view的size大小的修改
        if (optical != isLayoutModeOptical(mParent)) {
//有两种情况会进入到该if条件,
//一是子view有特殊的光学边界,而父view没有,此时optical为true,
//一种是父view有一个特殊的光学边界,而子view没有,此时optical为false

            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        //存储测量后的View宽 / 高的实际逻辑
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
//根据View宽/高的测量规格计算View的宽/高值
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:
            // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            //模式为AT_MOST,EXACTLY时,
            //使用View测量后的宽/高值 = measureSpec中的Size
            result = specSize;
            break;
        }
        return result;
    }

可以看出,View在当测量模式为UNSPECIFIED时,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小。其实这对我们自定义控件并没有什么影响,因为UNSPECIFIED模式是给系统内部用的。我们的重点还是应该放在AT_MOST和EXACTLY两种情况下。对于这两种情况,getDefaultSize十分简单粗暴,直接返回了specSize,也就是View的测量规格里的测量尺寸。

这里就出现了一个问题在AT_MOST和EXACTLY两种情况下返回的尺寸竟然都是specSize
因此在自定义View控件时,我们需要重写onMeasure方法并设置wrap_content时自身的大小。否则在xml布局中使用wrap_content时与match_parent的效果将会是一样

至此,单一View的宽/高值已经测量完成,即对于单一View的measure过程已经完成。
小小的总结一下,其是前面的源码只是为了对View的测量有个完整的概念,清楚整个流程,主要我们实现还是在onMeasure方法中,因此可以写一个自定义View的onMeasure方法的通用模版,其实最关键的也就是在AT_MOST模式时进行特殊处理,毕竟父类的onMeasure已经实现了大部分逻辑:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 必须调用,因为父类还是实现了很多东西的。
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 宽的测量模式
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
       // 宽的测量尺寸
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        // 高度的测量模式
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        // 高度的测量尺寸
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //根据View的逻辑得到,比如TextView根据设置的文字计算wrap_content时的大小。
        //这两个数据变量要根据实现需求计算。
        int wrapWidth,wrapHeight;
        
        // 如果有测量模式是AT_MOST则需要进行特殊处理
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            //长宽都是AT_MOST,则都需要计算
            setMeasuredDimension(wrapWidth, wrapHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            //只有宽是AT_MOST,则长用测量尺寸
            setMeasuredDimension(wrapWidth, heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            //只有长是AT_MOST,则宽用测量尺寸
            setMeasuredDimension(widthSpecSize, wrapHeight);
        }
}

ViewGroup的measure过程

ViewGroup的情况就复杂一些了,毕竟它还有一堆子View要考虑,大多数时候要先确定子View的大小,再确定ViewGroup的的大小。不过原理也很简单,就是遍历测量所有子View的尺寸然后将所有子View的尺寸进行合并,最终得到ViewGroup父视图的测量值。

看过源码就知道ViewGroup并没有重写View的onMeasure方法,为什么呢?显然这需要它的子类去根据相应的逻辑去实现,比如LinearLayout与RelativeLayout对child View的测量逻辑显然是不同的。这个也是单一View的measure过程与ViewGroup过程最大的不同

不过,ViewGroup倒是提供了一个measureChildren的方法,貌似可以用来测量child的样子,看看源码:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;//子View集合
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

这个方法的逻辑就很清晰嘛,就是遍历子View,调用measureChild方法对其进行测量,那么我们接着来看看measureChild方法:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //取出子View的LayoutParams
        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方法里,会取出child的LayoutParams,再结合父控件的测量规格已被占用的空间Padding,作为参数传递给getChildMeasureSpec方法,在getChildMeasureSpec里会组合生成child控件的测量规格。

getChildMeasureSpec是ViewGroup里提供的一个静态方法,用来用来获取子控件的测量规格。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //父view的测量模式
        int specMode = MeasureSpec.getMode(spec);
        //父view的测量大小
        int specSize = MeasureSpec.getSize(spec);
        //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
        int size = Math.max(0, specSize - padding);
        //子view想要的实际大小和模式(需要计算)  
        int resultSize = 0;
        int resultMode = 0;
        
        switch (specMode) {
        /// 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //如果child的布局参数有固定值(大于0),比如"layout_width" = "100dp"
                //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //当子view的LayoutParams为MATCH_PARENT时(-1)  
                //此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
                //子view大小为父view大小,模式为EXACTLY  
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //当子view的LayoutParams为WRAP_CONTENT时(-2) 
                //比如TextView根据设置的字符串大小来决定自己的大小
                //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
                //所以测量模式就是AT_MOST,测量大小就是父控件的size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 当父控件的测量模式是AT_MOST时,父view强加给子view一个最大的值
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也会优先满足孩子的需求
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                 //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
                //但同样的,child的尺寸上限也是父控件的尺寸上限size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //child想要根据自己逻辑决定大小,那就自己决定呗
                //同样的,child的尺寸上限也是父控件的尺寸上限size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

         // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
         // 多见于ListView、GridView  
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

总结下来逻辑如下:
!](https://upload-images.jianshu.io/upload_images/17755742-2a1a0809d563a88b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

至此,ViewGroup的测量流程也基本结束了,整体的流程如下图:


不过还有一个问题不知道大家注意到没有,measureChild方法只考虑了父View的padding,但是没考虑到子View的margin。这就会导致子view在使用match_parent属性的时候,margin属性会有问题。
当然,ViewGroup也考虑到了这个问题,为此也提供了另一个测量child的方法measureChildWithMargins:

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChildWithMargins方法,根据名字也能看出来,比measureChild方法多考虑了个margin,源码也跟前面的差不多,只是将margin考虑了进去,所以一般情况下,这个方法使用的更多一些。

至此,自定义View的中最重要、最复杂的measure过程就全部总结完了。下面就该Layout过程了。

onLayout

类似measure过程,layout过程根据View的类型分为2种情况,单一View和ViewGroup。对于单身View来说,一人吃饱全家不饿,调用layout方法确定好自己的位置,设置好位置属性的值(mLeft/mRgiht,mTop/mBottom)就行。而对于父母ViewGroup来说,还得通过调用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;

        //调用setFrame / setOpticalFrame方法来给View的四个顶点属性赋值,
        //即mLeft,mRight,mTop,mBottom四个值
        //判断当前View大小和位置是否发生了变化 & 返回 
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //调用onLayout方法
            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;
            //监听View位置变化
            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方法首先会调用isLayoutModeOptical这个方法,判断是否有光学边界的(光学边界这里暂时用不到,其实关键是我也不会,想深入了解的请自行谷歌),之后调用setFrame或者setOpticalFrame方法来给View的四个顶点属性赋值,即mLeft,mRight,mTop,mBottom四个值。那么我们再来看看setOpticalFrame方法:

 private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

可以看到,这个setOpticalFrame方法,最终也是调用了setFrame,那好我们可以直接继续看setFrame方法了:

protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //省略部分代码
            return changed;
    }

不难看出,setFrame中先比较了新位置和老位置是否有差异,如果有差异则会调用sizechanged来更新View的位置。

setFrame后这个View的位置就确定了。之后我们也就能通过调用getWidth()和getHeight()方法来获取View的实际宽高了。
然后,才会调用onLayout方法,由于单一View是没有子View的,因此在View类里的onLayout方法是个空方法。

另外,在layout方法的最后我们能看到一个OnLayoutChangeListener的集合,光看名字我们也知道,这是View位置发生改变时的回调接口。所以我们可以通过addOnLayoutChangeListener方法可以监听一个View的位置变化,并做出想要的响应。(不看源码根本不知道还有这样的方法。。。)

ViewGroup的layout过程

ViewGroup的layout过程就比View复杂一些了,大致分为两步,首先用layout方法计算自身ViewGroup的位置,之后在onLayout中遍历子View并且确定自身子View在ViewGroup的位置(调用子View 的 layout方法)

其实,ViewGroup的Layout的关键就是实现onLayout方法,在ViewGroup中onLayout方法被声明成了抽象方法,这就强制继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。

由于onLayout方法之前的流程和View是一样的,因此就不再赘述了。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     // changed 当前View的大小和位置改变了 
     // left 左部位置
     // top 顶部位置
     // right 右部位置
     // bottom 底部位置

     // 遍历子View:循环所有子View
          for (int i=0; i<getChildCount(); i++) {
              View child = getChildAt(i);   

              // 计算当前子View的四个位置值
                //位置的计算逻辑
                //TODO
                // 需自己实现,也是自定义View的关键

                // 对计算后的位置值进行赋值
                int mLeft  = Left
                int mTop  = Top
                int mRight = Right
                int mBottom = Bottom

              // 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
              // 即确定了子View在父容器的位置
              child.layout(mLeft, mTop, mRight, mBottom);
              // 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
          }
      }
  }

onDraw

终于,在测量完毕,布局完成之后,我们来到了View绘制的最后一步,那就是将View绘制到屏幕上。不论是View还是ViewGroup,都是调用draw方法完成绘制,我们来看看draw的源码:

public void draw(Canvas canvas) {
      //省略部分代码
      int saveCount;
      // 步骤1: 绘制本身View背景
      if (!dirtyOpaque) {
            drawBackground(canvas);
      }
      final int viewFlags = mViewFlags;
      if (!verticalEdges && !horizontalEdges) {

        // 步骤2:绘制本身View内容
        if (!dirtyOpaque) 
            onDraw(canvas);
        // View 中:默认为空实现,需复写
        // ViewGroup中:需复写

        // 步骤3:绘制子View
        // 由于单一View无子View,故View 中:默认为空实现
        // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
        dispatchDraw(canvas);

        // 步骤4:绘制装饰,如滑动条、前景色等等
        onDrawScrollBars(canvas);
        return;
    }
    //省略部分代码
}

和Measure以及Layout过程一样,View和ViewGroup是有些区别的,不过根据draw方法的源码来看,整体流程都是下面几个步骤:

  1. 绘制背景 -- drawBackground()
  2. 绘制自己 -- onDraw()
  3. 绘制孩子 -- dispatchDraw()
  4. 绘制装饰 -- onDrawScrollbars()
    View和ViewGroup最大的差别就是dispatchDraw()方法,由于View中不用考虑子View,那么dispatchDraw()就是一个空实现,而ViewGroup则必须要实现dispatchDraw()。

那么我们再来一步一步看看源码中都做了什么:
1. 绘制背景 -- drawBackground()

private void drawBackground(Canvas canvas) {
        //mBackground是该View的背景参数,比如背景颜色
        // 获取背景 drawable
        final Drawable background = mBackground;
        //没有背景则直接结束方法
        if (background == null) {
            return;
        }

        //根据在 layout 过程中获取的 View 位置的四个参数来确定背景的边界
        setBackgroundBounds();

        //省略部分代码

        //获取当前View的mScrollX和mScrollY值
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            // 调用 Drawable 的 draw 方法绘制背景
            background.draw(canvas);
        } else {
            //如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
            canvas.translate(scrollX, scrollY);
            // 调用 Drawable 的 draw 方法绘制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

2.绘制自己 -- onDraw()
由于不同的控件都有自己不同的绘制实现,所以View的onDraw方法肯定是空方法。在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容。也就是说我们在自定义View的时候要根据实际需求对onDraw方法进行实现。

3.绘制孩子 -- dispatchDraw()

protected void dispatchDraw(Canvas canvas) {
        ......

         // 1. 遍历子View
        final int childrenCount = mChildrenCount;
        ......

        for (int i = 0; i < childrenCount; i++) {
                ......
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                        // 绘制子View视图
                        more |= drawChild(canvas, transientChild, drawingTime);
                }
                ....
        }
    }
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
}

不难看出,dispatchDraw的逻辑就是遍历绘制子View,即遍历调用drawChild方法,drawChild方法又调用了child的draw(canvas, this, drawingTime)方法,最后还是调用到了child的draw(canvas)方法,这样绘制流程也就一层一层的传递下去了。

4.绘制装饰 -- onDrawScrollbars()

public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

这一步的目的是绘制装饰,如 滚动指示器、滚动条、和前景等。
至此,View的draw过程分析完毕。

总结

View的绘制流程可以总结为下图:


从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都需要实现该方法。

图片来源:Carson_Ho的自定义View
由于本人水平有限,若是文中有叙述不清晰或不准确的地方,希望大家能够指出,谢谢大家!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,572评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,071评论 3 414
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,409评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,569评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,360评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,895评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,979评论 3 440
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,123评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,643评论 1 333
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,559评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,742评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,250评论 5 356
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,981评论 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,363评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,622评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,354评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,707评论 2 370