美文网首页
View的工作原理和自定义

View的工作原理和自定义

作者: Vinson武 | 来源:发表于2020-02-26 13:51 被阅读0次

    初识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章

    相关文章

      网友评论

          本文标题:View的工作原理和自定义

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