美文网首页Viewviewandroid开发
自定义View基础之View的绘制流程

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

作者: Cris_Ma | 来源:发表于2017-04-23 12:56 被阅读100次

    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的源码,就一目了然了。
    <pre>
    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);
    }
    </pre>

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

    View的onMeasure方法以及MeasureSpec的获取

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

    ViewRootperformTraversals方法中可以看到:

    <pre>
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    </pre>

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

    <pre>
    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;
    }
    </pre>

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

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

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

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

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

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

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

    这一段代码有点长,但是不难理解,该方法在调用时传入了三个参数:父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方法就可以了:

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

    代码很简单,就是一个setMeasuredDimension,用来设置View的尺寸,传入的参数,在上文已经介绍的很清楚了,从ViewGroup传递过来。但是他还调用了一个getDefaultSize方法,我们来看一下:

    <pre>
    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;
    }
    </pre>

    这个方法逻辑也很简单,不考虑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()。代码如下

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

    </pre>

    首先去遍历子View,调用MeasureChild方法,在该方法中获取各个子View的MeasureSpec,然后调用子View的Measure方法,传递各个子View的大小。

    Layout过程

    Layout的作用,是ViewGroup为自己的子View指定位置,现在看一下View中的layout方法:

    <pre>
    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;
    }
    </pre>

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

    <pre>
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    </pre>

    ViewGroup

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

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

    layout方法的大致流程如下:

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

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

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

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

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

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

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

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

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

    <pre>
    final int childWidth = child.getMeasuredWidth();
    final int childHeight = child.getMeasuredHeight();
    </pre>

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

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

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

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

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

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

    此时获取到的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尺寸。

    Draw过程

    measure和layout的过程都结束后,接下来就进入到draw的过程了,源码如下:
    <pre>
    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;
    }
    }
    </pre>
    可以看到,第一步是从第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方法才会起作用。

    相关文章

      网友评论

        本文标题:自定义View基础之View的绘制流程

        本文链接:https://www.haomeiwen.com/subject/aoelzttx.html