Android自定义控件 —— 三大流程

摘要

自定义控件在工作中时常有所接触,但始终是只得其形,不得其神。最近抽空系统学习了下这方面的知识,准备用“Android自定义控件”作为一个博客系列来记录我的学习心得。系列文中不乏引用到Android SDK源码,以API 25为准。最后,如有雷同,没错,就是我去抄的。

Android自定义控件系列目录

  • Android自定义控件 —— 事件分发

  • Android自定义控件 —— 三大流程

View的三大流程分为:测量(measure)、布局(layout)、绘制(draw),下面就一一进行探索吧。

measure

布局绘画涉及两个流程:测量流程和布局流程,测量流程通过measure(int, int)实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout(int, int, int, int)来实现的,也是自顶向下的。在这个过程中,每个父容器ViewGroup负责通过计算好的尺寸放置它的子View。

前面讲过,onMeasure(int, int)是用来测量当前控件大小的,给onLayout(boolean, int, int, int, int)提供数值参考,需要特别注意的是,测量完成以后通过setMeasuredDimension(int,int)设置给系统。

MeasureSpec

这是一个View的内部类,介绍之前,先来看看源码

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        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(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

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

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }
    ...
}
作用

MeasureSpec是用来规范测量数值的。什么意思呢?MeasureSpec规定了,一个规范的测量数值(int型,32位)应该由mode和size两部分组成,即应该由一个32位的二进制数“mode(前两位) + size(后30位,即原始测量数值)”的形式组成。

mode分类

mode是用两位二进制表示的数,其分类如下

  • UPSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小

  • EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略子元素本身的大小

  • AT_MOST:子元素至多达到指定大小的值

它们对应的二进制值分别是

  • UNSPECIFIED = 00

  • EXACTLY = 01

  • AT_MOST = 10

应用

在XML布局时,通过android:layout_widthandroid:layout_height来设置控件的宽高值,宽高值分为三种类型:wrap_content、match_parent、具体值,三种值类型所对应的mode为

  • wrap_content ——> MeasureSpec.AT_MOST

  • match_parent ——> MeasureSpec.EXACTLY

  • 具体值 ——> MeasureSpec.EXACTLY

举个例子,测量的宽度值为50,那么其所对应的MeasureSpec规范数值就应该为:01 000000000000000000000000110010,其中,前两位(01)代表mode位,50对应的二进制数为110010,不足30位补位即可。

MeasureSpec和LayoutParams

上面了解了MeasureSpec这个类,那么该类是怎么创建来的呢?根据什么创建来的呢?现在就来深入了解一下。

对于普通View,其对应的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams来共同决定的。

普通View的measure过程由ViewGroup传递而来,那么来看看源码了解下MeasureSpec是怎么被创建出来的

package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    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);
    }
    ...
}

上述方法,首先获取到了子元素的LayoutParams,然后根据当前子元素的LayoutParams和父容器的MeasureSpec来获取子元素的 MeasureSpec,最后调用了子元素的measure(int, int)

来看看getChildMeasureSpec(int, int, int)的具体实现

package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    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);
    }
    ...
}

正如先前所说,上述方法主要是根据父容器的MeasureSpec同时结合子元素的LayoutParams来确定子元素的MeasureSpec。

由于UNSPECIFIED这个mode主要用于系统内部measure,一般来说,我们无须关注,所以排除掉UNSPECIFIED来总结一下就是

  • 当子View采用固定宽高的时候,不管父容器的MeasureSpec是什么,子View的mode都是EXACTLY,并且其大小遵循LayoutParams中的大小

  • 当子View的宽高设为match_parent的时候,如果父容器的mode为EXACTLY,那么子View的mode也是EXACTLY,并且大小是父容器的剩余空间。如果父容器的mode为AT_MOST,那么子View的mode也是AT_MOST,并且大小不会超过父容器的剩余空间

  • 当子View的宽高设为wrap_content的时候,不管父容器的mode是EXACTLY还是AT_MOST,子View的mode都是AT_MOST,并且大小不能够超过父容器的剩余空间

View的measure流程

View的测量流程是由measure(int, int)来完成的

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
    ...
}

发现该方法是一个final型方法,所以不能被重写。不过在其中回调了onMeasure(int, int),因此我们只需在View中重写onMeasure(int, int)方法来完成View的测量即可。那么View默认的onMeasure(int, int)实现是怎样的呢?

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    ...
}

可以看到,该方法的实现很简单,直接调用了setMeasuredDimension(int, int)来设置测量的尺寸。也就是说,关键就在于getDefaultSize(int, int)方法里

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    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;
    }
    ...
}

很显然,getDefaultSize(int, int)方法返回的就是MeasureSpec的specSize(源码里的变量,即实际测量值)部分,而这个MeasureSpec是ViewGroup传递过来的。

到这里也就理解了,为什么当我们在布局中设置子View的宽高值为wrap_content的时候,如果不重写其onMeasure(int, int)方法,则默认大小就是父容器的可用大小了。

当我们在布局中设置子View的宽高值为wrap_content时,那么测量模式(即mode)就是AT_MOST,在该模式下,它的宽高值等于specSize。而specSize由ViewGroup传递过来时就是parentSize(自定义的变量名称),也就是父控件的可用大小。

当我们在布局中设置子View的宽高值为match_parent时,那么不用多说,宽高值当然也是parentSize。这时候,我们只需对AT_MOST测量模式进行处理

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width = 0;
    int height = 0;

    if(widthMode == MeasureSpec.AT_MOST){
        width = ...
    }

    if(heightMode == MeasureSpec.AT_MOST){
        height = ...
    }

    setMeasuredDimension(widthMode != MeasureSpec.AT_MOST ? widthSize : width,
            heightMode != MeasureSpec.AT_MOST? heightSize : height);
}

上述代码,判断当测量模式是AT_MOST时,自己计算View的宽高。其他情况,直接使用specSize。

至于UNSPECIFIED这种情况,则是使用的是getDefaultSize(int, int)的第一个参数的值,也就是getSuggestedMinimumWidth()getSuggestedMinimumHeight()所返回的值,一般用于系统内部的测量过程,这两个方法的源码如下

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    ...
}

从源码看出,这是判断View有没有背景,没有背景的话,那么值就是View最小的宽度或高度,也就是对应XML布局中的android:minWidthandroid:minHeight属性,如果属性没有指定的话,默认值为0;有背景的话,那么值就是View最小的宽度或高度和背景的最小宽度或高度,取两者中最大的一个值,这个值就是当测量模式是UNSPECIFIED时View的测量宽/高。

到这里就完成了整个View的measure过程,完成之后我们就可以通过getMeasuredWidth()getMeasuredHeight()方法获取View正确的测量宽/高了。但是需要注意的是,在某些极端情况下,系统可能需要在多次measure过程后才能确定最终的测量宽/高,在这种情况下,直接在onMeasure(int, int)中获取的测量宽/高可能是不准确的,保险的做法是在onLayout(boolean, int, int, int, int)方法中去获取。

ViewGroup的measure流程

ViewGroup的measure流程和View不同,不仅需要完成自身的measure流程,还需要去遍历其所有子View,各个子元素再递归这个流程。ViewGroup提供了一个叫measureChildren(int, int)的方法

package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    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);
            }
        }
    }
    ...
}

该方法遍历了所有的子View,判断如果子View没有GONE掉的时候,就继续执行measureChild(View, int, int)方法

package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    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(int, int, int)创建子View的MeasureSpec,创建好后,将MeasureSpec传给子VIew进行其measure 流程。

解读源码发现,ViewGroup并没有定义其具体的测量流程(即实现onMeasure(int, int)),这是因为ViewGroup是一个抽象类,它的测量流程需要其子类去实现不同的布局特性,没办法做统一实现,比如说像LinearLayout、RelativeLayout等。

layout

通过源码发现,layout流程的关键在于onLayout(boolean, int, int, int, int),实现View的layout流程,就是实现该View的布局,而如果换成是ViewGroup,那么就实现其所有子控件的布局。

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    ...
}
package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
    ...
}

draw

下面直接通过查看draw(Canvas)的源码,来分析下其draw流程

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    public void draw(Canvas canvas) {
        ...

        /*
         * 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
        ...
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ...

        // Step 2, save the canvas' layers
        ...
        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ...

        if (drawTop) {
            ...
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            ...
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            ...
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            ...
            canvas.drawRect(right - length, top, right, bottom, p);
        }
        ...
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }
    ...
}

上面的源码注释写的很清晰,通过查看后我们了解到View的绘制共分为如下六步

  • 绘制背景

  • 如果需要,保存图层信息

  • 绘制View的内容

  • 如果View有子View,绘制View的子View

  • 如果需要,绘制View的边缘(如阴影等)

  • 绘制View的装饰(如滚动条等)

其中以上六步,第二步和第五步并不是必须的,所以我们只需重点分析其他四步即可。

绘制背景

绘制背景调用了drawBackground(Canvas)方法

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    private void drawBackground(Canvas canvas) {
        // 获取背景 drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
        setBackgroundBounds();

        ...

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

绘制内容调用了View#onDraw(Canvas)方法,由于View的内容各不相同,所以该方法是一个空实现,需要由子类去实现

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    protected void onDraw(Canvas canvas) {
    }
    ...
}
绘制子View

绘制子View调用了View#dispatchDraw(Canvas)方法,该方法同样是一个空实现

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    protected void dispatchDraw(Canvas canvas) {
    }
    ...
}

当只有包含子View的时候,才会去重写它,ViewGroup不正好符合条件吗? 来看下ViewGroup对该方法的实现吧

package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    protected void dispatchDraw(Canvas canvas) {
        ...
        final int childrenCount = mChildrenCount;
        ...

        for (int i = 0; i < childrenCount; i++) {
            ...
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            ...
        }
    }
    ...
}

ViewGroup#dispatchDraw(Canvas)方法的代码比较多,只分析重点,遍历了所有的子View并调用了ViewGroup#drawChild(Canvas, View, long)方法

package android.view;
...
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    ...
}

该方法最终还是调用了子View的draw(Canvas)方法。

由于ViewGroup已经为我们实现了该方法,所以我们一般都不需要重写该方法。

绘制装饰

绘制装饰调用了View#onDrawForeground(Canvas)方法,源码如下

package android.view;
...
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    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);
        }
    }
    ...
}

该方法默认实现是绘制了滚动指示器、滚动条、和前景。

参考链接:
1. Android View 的绘制流程
2. 自定义控件三部曲视图篇(一)——测量与布局
3. Android 从0开始自定义控件之深入理解 MeasureSpec (六)
4. Android 从0开始自定义控件之 View 的 measure 过程(七)
5. Andriod 从0开始自定义控件之 View 的 layout 过程 (八)
6. Android 从0开始自定义控件之 View 的 draw 过程 (九)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值