View 的工作流程---结合HenCoder教程和《Android开发艺术探索》的总结

Posted 范二er

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View 的工作流程---结合HenCoder教程和《Android开发艺术探索》的总结相关的知识,希望对你有一定的参考价值。

摘要:一直关注 Hencoder 的教程,前阵子刚好出了一期 View 的工作流程系列,然后结合《android开发艺术探索》相关章节,做一下笔记。

原文发表于本人的个人博客—–记录一个刚入职场的新人的学习过程


申明:文中 draw过程 这一小节,是摘抄自 HenCoder Android 开发进阶:自定义 View 1-5 绘制顺序 ,详细内容请点击链接查看。


MeasureSpec

在很大程度上,MeasureSpec 决定了一个 View 的尺寸,作所以说“很大程度上”,是因为这个过程还会受到父容器的影响,因为父容器会硬性 View 的 MeasureSpec 的创建过程。在测量过程中,系统会将 View 的 LayoutParams 根据父容器施加的规则转换成 MeasureSpec,然后再根据这个 MeasureSpec 来测量出 View 的宽高。

MeasureSpec 是一个 32 位的 int 值,高 2 位代表SpecMode,低30位代表SpecSize。

  • SpecMode:测量模式
  • SpecSize:在某一测量模式下的规格大小
    public static class MeasureSpec 
    ...
        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) 
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        

       public static int getSize(int measureSpec) 
            return (measureSpec & ~MODE_MASK);
        
   
   ...

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存,为了方便操作,其提供了打包和解包的方法。

SpecMode有三类,

UNSPECIFIED

父容器不对 View 有任何显示,要多大给多大,这种模式一般是用于系统内部绘制。

EXACTLY

对应于 LayoutParams 的 match_content 和具体数值两种情况。表示父容器已经检测出 View 所需要的精确大小,这个大小由 SpecSize 给出。

AT_MOST

对用于 LayoutParams 中的 wrap_content 模式。表示父容器制定了一个可用大小,即 SpecSize,View的大小不能大于这个值,具体是多少要看不同View的具体实现。

MeasureSpec 和 LayoutParams 的对应关系

在View 测量的时候,系统会将 View 的 LayoutParams 参数在父容器的约束之下转换成MeasureSpec,然后根据这个 MeasureSpec 来决定 View 测量后的宽高。什么叫做父容器的约束呢?也就父容器的 MeasureSpec,所以对于普通 View 来说,其 MeasureSpec 就是由父容器的MeasureSpec 和其自身的 LayoutParams 共同决定的。

上面说的是一个总结,这个总结,体现在ViewGroup的getChildMeasureSpec()方法中。


     /**
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    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 = 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);
    

这个方法看上去这么长,其实总结起来就是上面那句话:View 的 LayoutParams 参数在父容器的约束之下转换成 MeasureSpec。

这个方法是在 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);
    
  • 在第 7 行可以看到,调用了 getChildMeasureSpec 方法,这个方法传入的第一个参数是parentWidthMeasureSpec,第三个参数是lp.width,这就充分说明上面的总结:一个 View 的MeasureSpec和其本身的LayoutParams和父容器的 MeasureSpec 相关。
  • 最后一行,是拿到 View 的 MeasureSpecHeight 和 MeasureSpecWidth 值,去调用View的measure 方法,View 的 measure 方法放在下一节。

这么说起来,还是有点模糊,总结如下(也就是 getChildMeasureSpec 方法的表格呈现形式):

其中 parentSize 指的是父容器中当前可用的大小

按子View的LayoutParams总结如下:

  • 当View采用固定宽高的时候,无论父容器的SpecMode是什么,View的SpecMode都是EXACTLY,SpecSize遵循LayoutParams中的大小。
  • 当View采用match_parent时,如果父容器的SpecMode是EXACTLY,那么View的SpecMode也是EXACTLY,SpecSize是父容器的剩余空间;如果父容器的SpecMode是AT_MOST,那么View的SpecMode也是AT_MOST,并且SpecMode不会超过父容器的剩余空间。
  • 当View采用wrap_content时,无论父容器的SpecMode是什么,View的SpecMode都是AT_MOST,SpecSize不得超过父容器的剩余空间。

View的工作流程

View的工作流程主要包括measure、layout、draw三个,即测量布局和绘制,其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw将View绘制到屏幕上。

measure过程

measure() 方法被父 View 调用,在 measure() 中做一些准备和优化工作后,调用 onMeasure() 来进行实际的自我测量。 onMeasure() 做的事,View 和 ViewGroup 不一样:

  • View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
  • ViewGroup:ViewGroup 在 onMeasure() 中会调用所有子 View 的 measure() 让它们进行自我测量,并根据子 View 计算出的期望尺寸来计算出它们的实际尺寸和位置(实际上 99.99% 的父 View 都会使用子 View 给出的期望尺寸来作为实际尺寸)然后保存。同时,它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存;

那么久针对 View 和 ViewGroup 这两种情况分析了。

View 的 measure 过程

View 的 measure 过程是由其 measure 方法完成的,在这个方法中又会去调用 onMeasure 方法,onMeasure实现:


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

就是调用了一个 setMeasureDimension 方法,将 View 的宽高传递进去,这个方法在自定义 View 的时候经常用到,就是在确定了自定义 View 的宽高值之后,在 onMeasure 方法中最后调用的,用于确定自定义 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;
    
  • 就是根据 specMode 的不同值,返回不同的大小,当 AT_MOST 和 EXACTLY 模式下,就是返回 specSize 的值,也就是 View 测量后的大小。
  • 在 UNSPECIFIED 模式下,View 的大小就是 getDefaultSize 方法的第一个参数 size ,即宽高分别为 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个函数的返回值。在看下这两个函数(只贴出width的代码):

    protected int getSuggestedMinimumWidth() 
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    
  • 若 View 没有指定背景,那么 View 的宽度为 mMinWidth ,这个值是由 View 的 android:minWidth 属性指定的,若没有指定这个属性,那么这个 mMinWidth 为 0。
  • 若 View 指定了背景,那么 View 的宽度就是 mMinWidth 和 mBackground.getMinimumWidth() 两者中较大的一个。前者上面已经说了是什么,那么后者又是什么东西呢?mBackground 是一个 Drawable,那么点进 Drawable 里面去看就知道了:

    public int getMinimumWidth() 
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    

getMinimumWidth 返回的就是 Drawable 的原始高度,前提是这个 Drawable 有原始高度,不然就返回0;

ViewGroup 的 measure 过程

ViewGroup 是一个抽象类,因此他没有重写 View 的 onMeasure 方法,它提供了一个 measureChildren 方法:


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

这个方法就是调用 ViewGroup 的所有子 View 的 measureChild 方法,这个 measureChild方法如下:


    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 的 LayoutParams ,然后通过 getChildMeasureSpec 方法生成子 View 的 MeasureSpec,接着就将生成的 MeasureSpec 直接传递给子 View 的 measure 方法进行测量。 getChildMeasureSpec 的逻辑上述已经说明。

可以发现 ViewGroup 并没有定义其本身具体的测量过程,因为 ViewGroup 是一个抽象类, onMeasure 需要各个具体的子类去实现,不想 View 一样,对 onMeasure 方法做具体实现,是因为不同的 ViewGroup 的实现类,有不同的布局特性,这导致他们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 两者的布局特性显然不同。

layout 过程

  • layout 方法确定 View 本身的位置
  • onLayout 方法确定子 View 的位置

layout 的作用是 ViewGroup 用于确定子 View 的位置,当 ViewGroup 的位置确定了之后,它会在 onLayout 中遍历所有子 View ,并且调用其 layout 方法,而在子 View 的 layout 方法中,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;

        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);
            ...
            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);
                
            
        
            ...
    
  • 14 行调用了 setFrame 方法,这个方法就是用于确定 View 的四个顶点的位置,一旦四个顶点确定了,那么 View 在 ViewGroup 中的位置也就确定了。贴出 setFrame 中的一段代码,稍后用于说明问题。

    protected boolean setFrame(int left, int top, int right, int bottom) 
            ...
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            ...
    
  • 17 行,会调用 onLayout 方法,这个方法的用途是父容器确定子 View 的位置,和 onMeasure 方法类似,onLayout 的实现和具体的布局相关,所以 View 和 ViewGroup 都没有实现这个方法。看看 LinearLayout 中的 onLayout 方法:

    @Override
    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);
        
    
  • 这里分为竖直方向上的 layout 和水平方向上的 layout,这里看看 竖直方向上的:

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

    
  • 首先遍历竖直方向上的所有子 View ,并且调用 setChildFrame 方法来为子 View 确定位置
  • 注意 childTop 值会逐渐增加,这个增量包括分割线宽度、margin 值、childHeight,这样一来,在竖直方向上就符合 LinearLayout 的特性了。

接下来看看 setChildFrame 方法:


    private void setChildFrame(View child, int left, int top, int width, int height)         
        child.layout(left, top, left + width, top + height);
    
  • 就是直接调用子 View 的 layout 方法,这样 LinearLayout 是父容器,父容器在 layout 中完成自己的定位之后,就通过 onLayout 去调用子 View 的 layout 方法,让子 View 完成其对自身的 layout 过程,然后在子 View 的 layout 方法中,又会通过 onLayout 方法去调用下一级子 View 的 layout 方法… 这样一层一层的传递下去之后,就会遍历完整个 View 树。

测量宽高和最终宽高的区别

这个问题可以具体为:getMeasureWidth/height 和 getWidth/height 有什么区别。

前者很明显,就是 measure 过程中得到的宽高,那么重点在后者,先看看 View 中的 getWidth 方法:


    public final int getWidth() 
        return mRight - mLeft;
    

现在就是要搞清楚 mRight 和 mLeft 两个变量是在什么时候赋值的。

  • 还是看看 LinearLayout 的竖直方向的 layout 过程,也就是上面的 layoutVertical 方法,在第 9、10 行可以看到:

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
  • 然后在 22 行的 setChildFrame 方法,将 childWidth 和 childHeight 作为参数传入,

                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
  • 然后在 setChildFrame 中会去调用子 View 的 layout 方法,继续讲参数传递

child.layout(left, top, left + width, top + height);
  • 在 View 的 layout 方法中会调用 setFrame(l, t, r, b),这里的 l、t、r、b 和上面的参数对应,在 setFrame 中:

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

这段代码之前提到过,在这里,就将 mLeft mTop mRight mBottom 给赋值了,这个值就是在 LinearLayout 中通过 getMeasureWidth 和 getMeasureHeight 方法得到的。

  • 现在可以知道区别了在 View 的默认实现中,View 的测量宽高和最终宽高是相等的,只是两者的赋值时机不同,测量宽高形成于View 的 measure 过程,而最终宽高形成于View 的 layout 过程,在日常开发中,就可以认为 View 的测量宽高就等于 View 的最终宽高。

  • 一个好习惯就是:在 onLayout 方法中去拿 View 的测量宽高或者是最终宽高,因为在某些极端的情况下,系统需要经过多次的 measure 才能确定最终的宽高,这种情况下,在 onMeasure 方法中拿到的测量宽高可能是不准确的。

draw 过程

一个完整的绘制过程会依次绘制以下几个内容:

  1. 背景
  2. 主体(onDraw())
  3. 子 View(dispatchDraw())
  4. 滑动边缘渐变和滑动条
  5. 前景

一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

绘制背景

它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法),而不能自定义绘制

绘制主体

这个过程是在 onDraw 方法中执行的,但是在 View 中,这个方法是没有实现的,因为具体的 View 需要如何绘制,需要 View 的子类去具体的定制。所以当我们自定义 View 的绘制的时候,就就可以直接重写 onDraw 方法。


public class AppView extends View   
    ...

    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);

        ... // 自定义绘制代码
    

    ...

这里注意,是将自定义绘制的代码写在 super 的下面,不过这里写在 super 的上面和下面其实都是一样的, 因为上面提到,View 的这个方法是一个空实现,所以。

下面来讨论自定义 View 的绘制方法时,自定义的代码写在 super 上下的区别。

写在 super.onDraw() 的下面

把绘制代码写在 super.onDraw() 的下面,由于绘制代码会在原有内容绘制结束之后才执行,所以绘制内容就会盖住控件原来的内容。这是最为常见的情况:为控件增加点缀性内容。比如,在 Debug 模式下绘制出 ImageView 的图像尺寸信息:

写在 super.onDraw() 的上面

如果把绘制代码写在 super.onDraw() 的上面,由于绘制代码会执行在原有内容的绘制之前,所以绘制的内容会被控件的原内容盖住。 这种方式可以实现马克笔的效果:

绘制子 View

有部分的遮盖关系是无法通过 onDraw 方法来实现的,例如,你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀:


public class SpottedLinearLayout extends LinearLayout   
    ...

    protected void onDraw(Canvas canvas) 
       super.onDraw(canvas);

       ... // 绘制斑点
    

没毛病。

但是当添加了子 View 之后,


<SpottedLinearLayout  
    android:orientation="vertical"
    ... >

    <ImageView ... />

    <TextView ... />

</SpottedLinearLayout>  

造成这种情况的原因是 Android 的绘制顺序:在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。对于上面这个例子来说,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成之后,先前绘制的斑点就被子 View 盖住了。

具体来讲,这里说的「绘制子 View」是通过另一个绘制方法的调用来发生的,这个绘制方法叫做:dispatchDraw()。也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方法来绘制子 View

注:虽然 View 和 ViewGroup 都有 dispatchDraw() 方法,不过由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。

回到刚才的问题:怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让它的绘制代码在子 View 的绘制之后再执行就好了。

写在 super.dispatchDraw() 的下面

只要重写 dispatchDraw(),并在 super.dispatchDraw() 的下面写上你的绘制代码,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。


public class SpottedLinearLayout extends LinearLayout   
    ...

    // 把 onDraw() 换成了 dispatchDraw()
    protected void dispatchDraw(Canvas canvas) 
       super.dispatchDraw(canvas);

       ... // 绘制斑点
    

写在 super.dispatchDraw() 的上面

同理,把绘制代码写在 super.dispatchDraw() 的上面,这段绘制就会在 onDraw() 之后、 super.dispatchDraw() 之前发生,也就是绘制内容会出现在主体内容和子 View 之间。而这个……

其实和前面讲的,重写 onDraw() 并把绘制代码写在 super.onDraw() 之后的做法,效果是一样的。

onDrawForeground()

滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。

滑动边缘渐变和滑动条可以通过 xml 的 android:scrollbarXXX 系列属性或 Java 代码的 View.setXXXScrollbarXXX() 系列方法来设置;前景可以通过 xml 的 android:foreground 属性或 Java 代码的 View.setForeground() 方法来设置。而重写 onDrawForeground() 方法,并在它的 super.onDrawForeground() 方法的上面或下面插入绘制代码,则可以控制绘制内容和滑动边缘渐变、滑动条以及前景的遮盖关系。

写在 super.onDrawForeground() 的下面

如果你把绘制代码写在了 super.onDrawForeground() 的下面,绘制代码会在滑动边缘渐变、滑动条和前景之后被执行,那么绘制内容将会盖住滑动边缘渐变、滑动条和前景。


public class AppImageView extends ImageView   
    ...

    public void onDrawForeground(Canvas canvas) 
       super.onDrawForeground(canvas);

       ... // 绘制「New」标签
    


<!-- 使用半透明的黑色作为前景,这是一种很常见的处理 -->  
<AppImageView  
    ...
    android:foreground="#88000000" />

左上角的标签没有被前景遮盖住,而是保持了它本身的颜色

写在 super.onDrawForeground() 的上面

如果你把绘制代码写在了 super.onDrawForeground() 的上面,绘制内容就会在 dispatchDraw() 和 super.onDrawForeground() 之间执行,那么绘制内容会盖住子 View,但被滑动边缘渐变、滑动条以及前景盖住:


public class AppImageView extends ImageView   
    ...

    public void onDrawForeground(Canvas canvas) 
       ... // 绘制「New」标签

       super.onDrawForeground(canvas);
    

由于被黑色的前景给遮住了,这里看到的标签也是这种半透明的黑色

想在滑动边缘渐变、滑动条和前景之间插入绘制代码?

很简单:不行。

虽然这三部分是依次绘制的,但它们被一起写进了 onDrawForeground() 方法里,所以你要么把绘制内容插在它们之前,要么把绘制内容插在它们之后。而想往它们之间插入绘制,是做不到的。

draw() 总调度的方法

除了 onDraw() dispatchDraw() 和 onDrawForeground() 之外,还有一个可以用来实现自定义绘制的方法: draw()。

draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。前面讲到的背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。


// View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):

public void draw(Canvas canvas)   
    ...

    drawBackground(Canvas); // 绘制背景(不能重写)
    onDraw(Canvas); // 绘制主体
    dispatchDraw(Canvas); // 绘制子 View
    onDrawForeground(Canvas); // 绘制滑动相关和前景

    ...

从上面的代码可以看出,onDraw() dispatchDraw() onDrawForeground() 这三个方法在 draw() 中被依次调用,因此它们的遮盖关系也就像前面所说的——dispatchDraw() 绘制的内容盖住 onDraw() 绘制的内容;onDrawForeground() 绘制的内容盖住 dispatchDraw() 绘制的内容。而在它们的外部,则是由 draw() 这个方法作为总的调度。所以,你也可以重写 draw() 方法来做自定义的绘制。

写在 super.draw() 的下面

由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的下面,那么这段代码会在其他所有绘制完成之后再执行,也就是说,它的绘制内容会盖住其他的所有绘制内容。

它的效果和重写 onDrawForeground(),并把绘制代码写在 super.onDrawForeground() 下面时的效果是一样的:都会盖住其他的所有内容。

当然了,虽说它们效果一样,但如果你既重写 draw() 又重写 onDrawForeground() ,那么 draw() 里的内容还是会盖住 onDrawForeground() 里的内容的。所以严格来讲,它们的效果还是有一点点不一样的。

但这属于抬杠……

写在 super.draw() 的上面

同理,由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的上面,那么这段代码会在其他所有绘制之前被执行,所以这部分绘制内容会被其他所有的内容盖住,包括背景。是的,背景也会盖住它。

是不是觉得没用?觉得怎么可能会有谁想要在背景的下面绘制内容?别这么想,有的时候它还真的有用。

例如我有一个 EditText:

它下面的那条横线,是 EditText 的背景。所以如果我想给这个 EditText 加一个绿色的底,我不能使用给它设置绿色背景色的方式,因为这就相当于是把它的背景替换掉,从而会导致下面的那条横线消失:


<EditText  
    ...
    android:background="#66BB6A" />

在这种时候,你就可以重写它的 draw() 方法,然后在 super.draw() 的上方插入代码,以此来在所有内容的底部涂上一片绿色:


public AppEditText extends EditText   
    ...

    public void draw(Canvas canvas) 
        canvas.drawColor(Color.parseColor("#66BB6A")); // 涂上绿色

        super.draw(canvas);
    

draw 过程注意

关于绘制方法,有两点需要注意一下:

  1. 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。

  2. 有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。

draw 过程总结

另外别忘了上面提到的那两个注意事项:

  1. 在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false);
  2. 在重写的方法有多个选择时,优先选择 onDraw()。

总结

对 View 的绘制过程都清楚了之后,就可以进行各种自定义 View 了,Hencoder 说过,自定义 View 无非就是三个:绘制、布局、触摸反馈

其中绘制和布局这里总结了,在这两个操作过程中会大量使用到 Paint Canvas 和 Property Animation,这些后面再做总结。

参考引用

[1] 《Android 开发艺术探索》

[2] HenCoder Android 开发进阶:自定义 View 1-5 绘制顺序

以上是关于View 的工作流程---结合HenCoder教程和《Android开发艺术探索》的总结的主要内容,如果未能解决你的问题,请参考以下文章

推荐扔物线的HenCoder Android 开发进阶系列 后期接着更新

Android UI-薄荷健康尺子

Android 自定义 View 知识点

自定义View系列教程05--示例分析

如何将Reportlab与基于Django Class的View相结合?

Android View 的工作流程和原理