View的工作原理和自定义

初识ViewRoot和DecorView

  1. ViewRoot对应于ViewRootImpl类,,它是连接WindowManager和DecorView的纽带,view的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

  2. View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程最终将一个view绘制出来。

  • measure过程决定来View的宽/高,Measure以后,可通过getMeasureWidth和getMeasureHeight方法来获取测量宽高。
  • layout过程决定来View四个顶点的坐标和事件的View的宽高,layout完成后可通过getTop、getLeft等来获取四个顶点坐标,getWidth和getHeight来获取最终宽高。
  • draw过程则决定来View的显示,只有draw方法完成后,View的内容才能呈现到屏幕上。
  1. DecorView作为顶层View,一般情况下内部会包含一个竖直方向的LinearLayout,这个LinearLayout里面分上下两部分(titlebar和content)。我们在Activity中setContentView设置的布局就是驾到来id为content的FrameLayout中。可以通过ViewGroup content = findViewById(R.id.content)得到content,通过content.getChildAt(0)的到我们设置的View。DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后再传给我们的View。

DecorView被加载到Window中的过程

  • 从Activity的startActivity开始,最终调用到ActivityThread的handleLaunchActivity方法来创建Activity,首先,会调用performLaunchActivity方法,内部会执行Activity的onCreate方法,从而完成DecorView和Activity的创建。然后,会调用handleResumeActivity,里面首先会调用performResumeActivity去执行Activity的onResume()方法,执行完后会得到一个ActivityClientRecord对象,然后通过r.window.getDecorView()的方式得到DecorView,然后会通过a.getWindowManager()得到WindowManager,最终调用其addView()方法将DecorView加进去。
  • WindowManager的实现类是WindowManagerImpl,它内部会将addView的逻辑委托给WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在WindowManagerGlobal的addView()方法中不仅会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView通过root.setView()把DecorView加载到Window中。这里的ViewRootImpl是ViewRoot的实现类,是连接WindowManager和DecorView的纽带。View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联.

理解MeasureSpec

MeasureSpec

MeasureSpec代表一个32为int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        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);
            }
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        //...
    }

SpecMode有三类,每一类都有不同含义

  1. UNSPECIFIED

不确定模式,父容器不对View做任何限制,要多大给多大,这种模式一般用于系统内部,表示一种测量状态。

  1. EXACTLY模式

精确模式,父容器以及检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种。

  1. AT_MOST模式

最大值模式,父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

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

MeasureSpec和LayoutParams的对应关系

在View测量的时候,系统会将LayoutParams在父容器约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。MeasureSpec是由父容器的SpecMode和本身的LayoutParams共同决定的。

对于顶级view(DecorView)和普通view来说,MeasureSpec的转换过程略有不同。对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。 MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽高。

DecorView的MeasureSpec确定

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看到,是根据它的LayoutParams的参数来区分的:

  • LayoutParams.MATCH_PARENT:精确模式,大小是窗口的大小
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但不能超过窗口大小。
  • 固定大小:精确模式,大小为LayoutParams中指定的大小。

普通View的MeasureSpec确定

对于普通View,View的Measure过程由ViewGroup传递而来,先看一下ViewGroup的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);
    }

上述方法会对子元素进行measure,在调用子元素的measure方法前,会先通过getChildMeasureSpec来得到子元素的MeasureSpec。ViewGroup的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); //子 view剩余最大空间

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) { //parent的specMode
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) { //子view的LayoutParams是具体值
                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);
    }

上述方法主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。

普通View的MeasureSpec的创建规则如下:

 

image.png

可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,即可以快速确定出子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。

View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程,其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置(即确定view的位置),draw将View绘制到屏幕上。

View的绘制流程之measure

measure的过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成其测量,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,然后各个子View再递归这个过程。

  1. View的measure过程

View的measure过程由其measure方法来完成,它是一个final方法,意味子类不能重写。在measure方法中会调用onMeasure方法:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //...
        onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

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

setMeasuredDimension方法会去设置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;
    }

我们只要看.AT_MOST和EXACTLY模式,可以简单理解,其实getDefaultSize返回的大小就是measureSpec的specSize。

对于UNSPECIFIED的情况,一般用于系统内部的测量过程。这种情况getDefaultSize第一个参数size由getSuggestedMinimumWidth()返回

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

可以看到,如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。
mBackground.getMinimumWidth()返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则返回0.

从getDefaultSize方法的实现来看,View的宽高由specSize决定,所以,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。原因,结合代码和MeasureSpec的确定规则知,如果View在布局中使用wrap_content,那么它的SpecMode是AT_MOST,在这种模式下,它的宽高等于specSize也就是parentSize,效果和match_parent是一样的。解决办法就是给View指定一个默认宽高,并在wrap_content时设置宽高即可。

@Override
    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);
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) { //wrap_content时的默认宽高
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }
  1. ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历调用所有子元素的measure方法。和View不同的是,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);
            }
        }
    }
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);
    }
  • 首先,在ViewGroup中的measureChildren()方法中会遍历测量ViewGroup中所有的View,当View的可见性处于GONE状态时,不对其进行测量。
  • 然后,测量某个指定的View时,根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec
  • 最后,将计算出的MeasureSpec传入View的measure方法

这里ViewGroup没有定义测量的具体过程,因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类(比如LinearLayout)去实现。不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法。(setMeasureDimension方法用于设置View的测量宽高,如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高)

LinearLayout的onMeasure方法实现解析(只看Vertical模式)

系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。

在Activity启动时获取某个View的测量宽高

由于View的measure过程和Activity的生命周期方法不是同步执行的,如果View还没有测量完毕,那么获得的宽/高就是0。所以在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息。解决方式如下:

  • Activity/View#onWindowFocusChanged:此时View已经初始化完毕,当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。
public void onWindowFocusChanged(boolean hasFocus){
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}
  • view.post(runnable): 通过post可以将一个runnable投递到消息队列的尾部,始化好了然后等待Looper调用次runnable的时候,View也已经初始化好了。
protected void onStart(){
    super.onStart();
    view.post(new Runnable(){
        @Overrie
        public void run(){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
  • ViewTreeObserver#addOnGlobalLayoutListener:当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调。
protected void onStart(){
    super.onStart();
     ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
}
  • View.measure(int widthMeasureSpec, int heightMeasureSpec):match_parent时不知道parentSize的大小,测不出;具体数值时,直接makeMeasureSpec固定值,然后调用view..measure就可以了;wrap_content时,在最大化模式下,用View理论上能支持的最大值去构造MeasureSpec是合理的。
int widthMSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMSpec, heightMSpec);

View的绘制流程之Layout

layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。

先看ViewGroup的

 @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b); //调用View的layout方法
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b); //抽象方法,子类必须自己实现布局

可以看到ViewGroup的layout方法实际还是调用View的layout方法,而onLayout则是一个空的抽象方法,子类自己实现。

在看View的layout方法:

@SuppressWarnings({"unchecked"})
    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); //会调setFrame方法

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b); //onLayout方法
            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;
    }
    //onLayout也是一个空方法
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

layout方法大致流程:首先会通过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置。然后,会执行到onLayout空方法,这个方法的用途是父容器确定子元素的位置。(即先确定自己本身位置,再遍历确定子元素位置)子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup中所有View控件布局流程。

LinearLayout的onLayout方法实现(Vertical方向)

其中会遍历调用每个子View的setChildFrame方法为子元素确定对应的位置。其中的childTop会逐渐增大,意味着后面的子元素会被放置在靠下的位置。而setChildFrame方法只是调用子view的layout方法而已。这样父元素在layout方法中完成自己的定位,并通过onLayout方法去调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置,这样一层一层传递完成整个View树的layout。

==问题==:View的测量宽高和最终宽高有什么区别?

在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。 在一些特殊的情况下则两者不相等:

  • 比如重写View的layout方法,使最终宽度总是比测量宽/高大100px。
  • View需要多次measure才能确定自己的测量宽/高,在前几次测量的过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。

View的绘制流程之Draw

绘制基本上可以分为六个步骤:

  • 首先绘制View的背景(canvas);
  • 如果需要的话,保持canvas的图层,为padding做准备;
  • 然后,绘制View的内容(onDraw);
  • 接着,绘制View的子View(dispatchDraw);
  • 如果需要的话,绘制View的padding边缘并恢复图层;
  • 最后,绘制View的装饰(onDrawScrollBars例如滚动条等等)。
 public void draw(Canvas canvas) {
        if (mClipBounds != null) {
            canvas.clipRect(mClipBounds);
        }
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * 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)
         */
         //...
         // skip step 2 & 5 if possible (common case)
        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); //绘制子View

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // we're done...
            return;
        }
        //...

protected void dispatchDraw(Canvas canvas) {
//View中是空的,由子 View实现,比如ViewGroup中由自己的实现
    }

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层传递来下去。

View中setWillNotDraw的作用

 public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

如果一个View不需要绘制任何内容,那么设置WILL_NOT_DRAW这个标记位为true以后,系统会进行相应的优化。

  • 默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。
  • 当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。
  • 当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。

自定义View

自定义View分类

  1. 继承View重写onDraw

主要用于实现一些不规则的效果。采用这种方式需要自己在onMeasure支持wrap_content,并且padding也需要在onDraw自己处理。

  1. 继承ViewGroup派生特殊layout

用于实现自己定义的新布局。需要合适地处理ViewGroup的测量、布局两个过程,同时处理子元素的测量和布局过程。

  1. 继承特定的View(比如TextView)

用于扩展某种已有的View的功能。这种方法可以不需要自己支持wrap_content和padding。

  1. 继承特定的ViewGroup(比如LinearLayout)

用于扩展某种布局。不需要自己处理ViewGroup的测量和布局这两个过程。

自定义View须知

  1. 让View支持wrap_content

直接继承View或ViewGroup的控件,如果没在onMeasure中对wrap_content做处理,那么wrap_content会无效。

  1. 如果有必要,支持padding

直接继承View的控件,如果不再onDraw中处理padding,那么padding属性无法起作用。直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑自身padding和子元素margin对其造成的影响,不如会无效。

  1. 尽量不要在View中使用Handler,没必要

View内部本身提供来post系列方法,完成可以替代Handler

  1. View中如果又线程或动画,要及时停止,参考View#onDetachedFromWindow

当包含此view的Activity退出或当前View被remove时,View的onDetachedFromWindow方法会被调用,在这个方法停止线程或动画是很好的时机。

  1. View带嵌套,需处理好滑动冲突

View有嵌套情形,需要自己处理好滑动冲突(外部拦截法或内部拦截法)

自定义View示例

image.png

1. 继承View重写onMeasure和onDraw方法

这种方式需要自己在onMeasure处理wrap_content和在onDraw处理padding。

public class CircleView extends View {

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //处理自定义属性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); //CircleView_circle_color是属性集_属性使用_连接起来的固定格式
        a.recycle();
        
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    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);
        //支持wrap_content,默认值200
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //处理padding
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
                radius, mPaint);
    }
}

添加自定义属性步骤:

  • 在values目录下创建自定义属性XML文件attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CircleView"> //属性集合
        <attr name="circle_color" format="color" /> //具体属性
    </declare-styleable>

</resources>
  • 在View的构造方法解析自定义属性并做处理
  • 在布局文件中使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" //使用自定义属性必须声明,app是自定义的名称,可改
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical" >

    <com.ryg.chapter_4.ui.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"
        android:padding="20dp"
        app:circle_color="@color/light_green" /> //“app”和上面定义的一致即可

</LinearLayout>

2. 继承ViewGroup派生layout

需要合适处理ViewGroup的测量、布局以及子元素的测量布局过程,这里只看onMeasure和onLayout方法,完整可参看github

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

这里两点不规范,一是没有子元素时不应该把宽高设为0,而应该根据LayoutParams来处理;第二点在测量时没有考虑它的padding和子元素的margin。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;
//遍历子View,调用子View的layout方法
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

同样没有考虑它的padding和子元素的margin占用

Requestlayout,onlayout,onDraw,DrawChild区别与联系?

  • requestLayout()方法 :会导致调用 measure()过程 和layout()过程,将会根据标志位判断是否需要ondraw。
  • onLayout()方法:如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局。
  • onDraw()方法:绘制视图本身 (每个View都需要重载该方法,ViewGroup一般不需要实现该方法)。
  • drawChild():去重新回调每个子视图的draw()方法。

invalidate() 和 postInvalidate()的区别 ?

  • invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用

参考书籍《Android开发艺术探究》第4章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vinson武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值