自定义View基础之View的绘制流程

DecorView

在了解view的绘制流程之前,首先我们要知道一个DecorView的概念,什么是DecorView?

DecorView是整个界面的最顶层View,它的尺寸通常就是屏幕尺寸,也就是说,DecorView是充满屏幕的,它实际上是一个FrameLayout,又包含了一个子元素,LinearLayout,这个LinearLayout又包含两个FrameLayout,一个用来显示标题,一个用来显示内容。显示内容的FrameLayout,其ID为 android.R.id.content

我们在 Activity 中设置 Layout 时,用的方法是setContentView,指的就是这个content。参考下图

DecorView

View的绘制流程是从ViewRootperformTraversals开始的,它经过measure,layout,draw三个过程最终将View绘制出来。

performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法,他们会依次调用measure,layout,draw方法,然后又调用了onMeasureonLayoutdispatchDrawonMeasure方法中,父容器会对所有的子View进行Measure,子元素又会作为父容器,重复对它自己的子元素进行Measure,这样Measure过程就从DecorView一级一级传递下去了。Layout和Draw方法也是如此。

我们关注的重点是onMeasure方法,它决定了所有View的尺寸。

MeasureSpec

MeasureSpec是一个32位 int 数值,它包含了两组信息。高两位代表SpecMode,低30位代表SpecSize

SpecMode指测量模式,有以下三个值:

  • EXACTLY
    表示父容器已经确定好子view的大小值,这时候view的大小就是SpecSize的值。它对应了LayoutParams中设置 match_parent 和指定具体数值的情况
  • AT_MOST
    表示父容器并不确定子View的具体大小,但是确定了一个上限,就是SpecSize,子View不能超过这个大小。具体值是多少,要看View 的具体实现。
  • UNSPECIFIED
    父容器不对子View进行限制,可以按照自己的意愿随意设置,比较少见,通常用于系统内部。

说完这三种模式可能大家还是非常模糊,我们看一下MeasureSpec的源码,就一目了然了。

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;
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

MeasureSpec 内部封装了makeMeasureSpecgetModegetSize三个方法,方便我们对MeasureSpec数据进行处理。
那么MeasureSpec到底是怎样使用的呢?接下来我们就要看onMeasure方法了。

View的onMeasure方法以及MeasureSpec的获取

首先我们回顾一下View 的绘制流程,在上文中有一句黑体显示的话,意思就是所有的View测量都是从最顶层的DecorView开始的,我们就先看一下DecorView的Measure过程,它的MeasureSpec是怎样得到的。

ViewRootperformTraversals方法中可以看到:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 

DecorViewMeasureSpec是通过getRootMeasureSpec来得到的,它传入了两个参数,lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。
然后来看一下getRootMeasureSpec的代码:

private int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  
    case ViewGroup.LayoutParams.MATCH_PARENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
} 

到这里,我们就知道了,DecorView作为最顶级的根View,它的MeasureSpec就是EXACTLY+WindowSize,也就是说他总是充满全屏的。

最顶层的DecorView尺寸确定好之后,下一步就是各个子View的Measure过程了,系统会从ViewGroup开始一级一级向下Measure。但是,我们又知道,一个View的大小同时还要受到父View的限制,它的大小是由本身的LayoutParams,和父View 的MeasureSpec共同决定的。

对于普通的View(非ViewGroup),它的Measure过程是由ViewGroup传递过来的,看一下ViewGroup的measureChildWithMargin方法就清楚了:

    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);
    }

可以看到,子View的Measure方法,实际上就是在这里调用的。在Measure之前,先获得子View的MeasureSpec,然后调用了child.measure。

子View的MeasureSpec又是通过getChildMeasureSpec来获取的,代码如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent asked to see how big we want to be
        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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这一段代码有点长,但是不难理解,该方法在调用时传入了三个参数:父View的MeasureSpecPadding(父View已经被占用的空间),和子View的LayoutParams(match_parent,wrap_content,或者精确的数值)

然后根据父view的SpecMode进行判断,用表格的方式表示如下,横排表示父View的SpecMode,竖排表示子View的SpecMode(LayoutParams),内容表示View最终的SpecMode

EXACTLY(match_parent给定数值) AT_MOST(wrap_content) UNSPECIFIED
dp/px( >0) EXACTLY+给定值 EXACTLY+给定值 EXACTLY+给定值
MATCH_PARENT EXACTLY+父View剩余空间(或0) AT_MOST+父View剩余空间(或0) UNSPECIFIED + 0
WRAP_CONTENT AT_MOST+父View剩余空间(或0) AT_MOST+父View剩余空间(或0) UNSPECIFIED + 0

到这里就很清楚了,子View的大小是由父View的MeasureSpce和它本身的LayoutParams共同决定的:

  • View是具体数值:最终结果一定是EXACTLY+·给定值·
  • 子·View·是MATCH_PARENT,它的大小就是父View的剩余空间,mode和父View相同(不考虑UNSPECIFIED )
  • ViewWRAP_CONTENT,它的大小是父View的剩余空间,mode是AT_MOST

Measure的流程到这里基本已经清楚了:从顶级View开始,先调用MeasureChild将父View的Spec传入,通过getChildMeasureSpec方法,获取到子View的Spec,然后通过child.measure(widhSpec,heightSpec)将得到的子View Spec传递过去。

View的Measure过程

通过上边的介绍,我们已经确定了View的MeasureSpec,接下来就是具体的Measure方法了,因为View的Measure最终调用的是onMeasure,我们只要看onMeasure方法就可以了:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

代码很简单,就是一个setMeasuredDimension,用来设置View的尺寸,传入的参数,在上文已经介绍的很清楚了,从ViewGroup传递过来。但是他还调用了一个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;
    }

这个方法逻辑也很简单,不考虑UNSPECIFIED的情况下,最终返回的结果一定是在ViewGroup里传递来的MeasureSpec

到这里,整个Measure过程就已经结束了。View的最终尺寸大小,遵循的就是上面我们得出的三条结论。要注意一点,当我们直接继承View时:

子View是WRAP_CONTENT,它的大小是父View的剩余空间,mode是AT_MOST

前两个都是没有问题的,但是WRAP_CONTENT,它的大小和MATCH_PARENT是一样的,也就是说WRAP_CONTENT会不起作用。解决这个问题也很简单,必须重写onMeasure方法,然后自定义一个默认的width和height,当传递过来的SpecModeAT_MOST时,设置尺寸为定义的宽高。

ViewGroup的Measure过程

相对于单一View来说,ViewGroupMeasure方法要复杂一些,因为它不仅仅是确定自己的尺寸,还要测量每一个子View,并得到他们的MeasureSpecViewGroup并没有重写Measure(final的)方法,也没有重写onMeasure方法,因为ViewGroup是抽象类,onMeasure方法需要在具体的实现类中去重写。

ViewGroup只是为测量子View添加了两个新的方法: measureChildren()measureChild()。代码如下

    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);
            }
        }
    }
-----------------------------------
    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,调用MeasureChild方法,在该方法中获取各个子View的MeasureSpec,然后调用子View的Measure方法,传递各个子View的大小。

Layout过程

Layout的作用,是ViewGroup为自己的子View指定位置,现在看一下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;
        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;
    }

关键在第11行,它最终调用了onLayout(changed, l, t, r, b)方法,来完成最终的Layout过程。
那么我们就看一下onLayout方法,你会发现是这样的:
View:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

ViewGroup

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

View中是一个空方法,ViewGroup中是一个抽象方法。为什么呢?其实答案很简单。Layout过程是确定子元素在自己布局中的位置,view是不存在子元素的,所以是空方法,它只需要通过setFrame来决定自身的位置。而ViewGroup本身就是一个抽象方法,它的不同实现会有不同的Layout方式,所以在继承ViewGroup的布局中,必须指定自己的Layout方式,因此,ViewGroup中是一个抽象方法。

layout方法的大致流程如下:

首先通过setFrame来设置View的四个顶点位置,并保存起来。在Layout时,会先用setFrame方法来保存四个顶点的坐标,并进行判断,值如果发生了变化,就会调用onLayout方法来重新定位子元素。
如果是单一View,调用setFrame方法之后,其实就已经结束了,因为他没有子元素,onLayout方法是空的,调用onlayout方法没有任何意义。而对于ViewGroup来说,他是包含子元素的,需要在onLayout方法中继续确定子元素的位置。

借用一下LinearLayout的源码来分析:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

可以看见,VERTICALHORIZONTAL的方法是不一样的。看一下layoutVertical的代码:

    void layoutVertical(int left, int top, int right, int bottom) {
        ......
        final int count = getVirtualChildCount();
        ......
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                ......
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
                i += getChildrenSkipCount(child, i);
            }
        }
    }

源代码比较长,我们只截取一部分,其实逻辑很简单,就是遍历所有的子元素,并调用setChildFrame方法来确定各个子元素的位置,因为是Vertical的排列方式,所以childTop 的值不断增大,子元素会依次向下排列。
setChildFrame代码如下:

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

只是调用子View的Layout方法而已。View的Layout方法上边已经介绍过了,如果是单一View,调用setFrame结束,如果是ViewGroup,会继续调用他的onlayout方法,这样一层一层向下确定各个View的位置。整个流程结束以后,所有的Layout过程就结束了。

可能大家也注意到一个问题,layout方法会接收四个参数,分别代表四个顶点的位置,而该方法是通过child.layout(left, top, left + width, top + height);来调用的,实际上,只有left和top两个值是确定的
另外两个顶点是通过宽和高来确定的,宽和高是这样获取的:

                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

得到四个顶点的值之后,最终会传递到setFrame方法,setFrame的部分代码:

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

这样看起来,view最终的尺寸就是Measure过程中确定的尺寸
如果我们改动一下代码,改变一下ViewGroup的setChildFrame或者view的layout方法:

child.layout(left, top, left + width+100, top + height+100);
或者
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b+100);
}

这样最终得到的View就会比Measure出来的View大了100的尺寸,在setFrame的时候,得到的四个坐标值也会大100,
看一下View的getwidth和getHeight代码:

    public final int getWidth() {
        return mRight - mLeft;
    }
    public final int getHeight() {
        return mBottom - mTop;
    }

此时获取到的View最终宽和高就会比Measure出来的宽和高(getMeasuredWidth,getMeasuredHeight)要大100px了。
所以,Measure出来的尺寸,通常情况下是View的最终尺寸,实际上View的最终尺寸是在Layout阶段来决定的,它并不一定等于MeasureSpec的大小。

获取View的尺寸

上边我们提到了两个方法:getMeasuredWidthgetWidth,用来获取View的尺寸,我们知道,View的尺寸是通过MeasureLayout共同决定的,那么获取View的尺寸就很简单了,调用这两个方法就可以了。实际上并不是这么简单的。因为Activity的生命周期和View的绘制不是同步的。在onCreateonStartonResume里简单的调用这两个方法,得到的结果是不确定的,因为View可能还没有绘制完成。那么怎样才能得到正确的View尺寸?

1.重写View的onFocusChanged方法。
在View得到或者失去焦点的时候,该方法都会被调用。可以这么理解,既然View已经得到焦点了,那么它的宽和高必定已经准备好了。所以获取尺寸是完全可以的。所以我们可以重写该方法,在此处调用getMeasuredWith方法。

2.利用view.post(runnable)
View在处理post的消息时,肯定已经初始化好了,所以利用此方法也能正确的获取到View尺寸。

3.ViewTreeObserver
它是view事件的一个观察者,用来监听ViewTree的各种事件,针对不同的事件它定义了许多不同的接口,可以利用onGlobalLayout方法来获取View尺寸。

4.Activity的onPostCreate()

Draw过程

measure和layout的过程都结束后,接下来就进入到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过程确定的视图位置来设置背景的绘制区域,之后再调用Drawabledraw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()setBackgroundResource()等方法进行赋值。

接下来的第三步是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroupdispatchDraw()方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。

通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextViewImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。

另外,view还有一个特殊方法:setWillNotDraw。默认情况下,view是不启用这个参数的,而viewGroup会启用。因为单一View是肯定需要绘制自身的,而ViewGroup只是作为单一View的载体,draw工作是交给子View的,它不需要绘制自身,也就是默认ViewGroup是透明的,如果想让ViewGroup绘制自身,需要调用setWillNotDraw(false),来启用draw功能,然后重写的onDraw方法才会起作用。

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

推荐阅读更多精彩内容