美文网首页
Android开发艺术(4)——View的工作原理

Android开发艺术(4)——View的工作原理

作者: X_Sation | 来源:发表于2017-11-13 20:23 被阅读14次

    初识ViewRoot和DecorView

    低版本——2.3中是ViewRoot,高版本——4.0以上是ViewRootImpl,看名字感觉跟是View的root,实际跟View没有这种关系,View树的根是DecorView,DecorView又是在PhoneWindow中的。

    WindowManagerImpl中的addView()中有一段这样的代码root.setView(view, wparams, panelParentView);,root就是ViewRootImpl,所以ViewRootImpl其实是view树的管理者,他把DecorView和WindowManager联系起来,它用来管理View树

    ViewRootImpl中的performTraversals方法是View绘制的起始点(View的绘制流程是measure、layout、draw)

    performTraversals中:

    • 先调用performMeasure然后内部调用, mView.measure,内部调用onMeasure(5.0、4.0版本中略有差异)
    • performLayout同上
    • performDraw同上,不过通过dispatchDraw分发draw而不是onDraw(onDraw是画自己,dispatchDraw是画孩子)

    关于view的三个流程:

    • measure之后,就可以通过getMeasureWidth获取到测量的宽高,一般情况他是view的宽高。(之所以说是一般情况是因为看下面)
    • layout之后,view的左上右下就固定了,可以通过getLeft/getRight/getTop/getBottom获取到这些信息,getWidth就是根据这个计算的。
    • draw之后,view才会可见

    DecorView是根View,是FrameLayout,内部通常是Linearlayout,然后里面是titlebar、… 、content,content就是我们setContentView所设置的View的容器。具体这个结构还和Activity主题有关

    理解MeasureSpec

    MeasureSpec是一个类,它可以描述成一个32位的二进制数,高二位表示MODE,是测量模式(未知、精确、取最大三种,对应的英文就不说了),低30位是SIZE(数字、包裹内容、匹配父容器,英文也不说了,就是xml中我们常用的三种)

    通过各种逻辑与或非操作,可以把两个数字mode和size包装成一个MeasureSpec,还可以把MeasureSpec解包成mode和size,这些操作谷歌已经写好了,直接可以用

    一个View,有MeasureSpec,通过MeasureSpec,调用measure(),然后调用onMeasure(),在onMeasure中,对于普通的view,直接就可以完成测量,对于ViewGroup,还要测量他的孩子(注意,是测量的宽高,上一小节说了,最终的宽高在layout之后才能确定,不过一般情况是一样的)

    那么MeasureSpec是怎么来的呢?MeasureSpec是View自身的LayoutParams结合父亲的一些规则(其实就是父View的MeasureSpec,毕竟父View的MeasureSpec就可以决定父View的测量宽高了,就像这个View的MeasureSpec确定之后,测量宽高就确定了),计算来的,换句话说就是,给一个View设置Layoutparams,不能保证他在任何情况下都这么大,因为还和他的父View有关系。(注意,还有一种特殊情况,DecorView没有父View,所以他的MeasureSpec是自己的Layoutparams和窗口——Window的尺寸决定的,DecorView是在Window中的,之前说过)。具体的DecorView、普通View,他们的MeasureSpec是如何创建的呢,下面就来分析:

    • DecorView的MeasureSpec创建:

      DecorView的MeasureSpec是在ViewRootImpl中创建的,在measureHierarchy中调用getRootMeasureSpec创建,看代码可知,这个MeasureSpec是由DecorView的layoutparams和窗口大小决定的

    //注:这两个参数分别是窗体宽高、DecorView的layoutparams的width
    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;
        }
    

    measureHierarchy中创建完成宽高的MeasureSpec之后,就开始执行performMeasure

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    
          childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
          childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
          //内部调用decorView的measure方法
          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
            return windowSizeMayChange;
    }
    
    • 普通View的MeasureSpec的创建:

      DecorView的MeasureSpec是从ViewRootImpl中创建的,然后让DecorView调用measure方法

      普通的View是在他的父View中创建MeasureSpec,然后让子View(自己)调用measure方法

      和ViewRootImpl中的measureHierarchy类似,在ViewGroup中有个measureChildWithMargins方法,里面通过getChildMeasureSpec创建孩子的MeasureSpec,然后调用child.measure(),整个过程跟ViewRootImpl中的measureHierarchy很相似

    protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //这里可知,孩子的measureSpec的创建是由父View的measurespec、父view的padding,以及孩子自己的layoutparams(margin、width等)决定的
            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);
        }
    

    重点是getChildMeasureSpec,下面这段代码需要十分熟悉:

    //三个参数分别是
    //1.父View的measurespec
    //2.父View已经占用的尺寸,也就是孩子不能使用的(这个是父View的padding+孩子的margin,看上面那段代码可知)
    //3.子view的width(MATCH_PARENT、WARP_CONTENT、具体数值)
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            //父容器的可用尺寸(去掉了padding),如果是负的,那就是0
            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:
                //孩子的尺寸是具体数值(大于等于0就是具体数值)
                if (childDimension >= 0) {
                    //如下
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                  //孩子是MATCH_PARENT
                } 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.
                  //孩子是包裹内容,那么孩子的测量模式就是AT_MOST,并且此时size的含义就是孩子最大可能的尺寸,而不是孩子的具体尺寸了
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
                //父亲是AT_MOST,说明父亲的尺寸不确定,但是父亲最大不能超过某个数值,这个数值是已知了
            case MeasureSpec.AT_MOST:
                //孩子是具体数值,那孩子就像下面那样,精确模式、具体尺寸
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                  //孩子是MATCH_PARENT,那么孩子不是精确的,但是孩子可以确定他最大尺寸,那就是父亲的最大尺寸,模式是AT_MOST
                } 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:
                //这种情况
                break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    网上各种表就不看了,上述代码理解了,其他表什么的都是浮云

    综上:View的创建过程是在它的父View中的,DecorView是特殊的,没有父View,也不是在PhoneWindow中,而是在ViewRootImpl中,ViewRootImpl是DecorView和WindowManager的纽带,所以他引用了DecorView和WindowManager

    View的工作流程

    view的工作流程主要指的是:measure、layout、draw,其中measure最为复杂,一个一个来分析

    Measure

    View的测量分两种情况:View和ViewGroup,对于View测量完就好了。对于ViewGroup,还要测量孩子。

    View的测量

    由上一节可知,在View的父容器中会为View创建MeasureSpec,然后child.measure(),就进行测量了。

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

    measure是一个final,所以没法重写,内部调用了onMeasure()

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      //这个方法就是给View设置测量值,所以着重看参数是如何拿到的getDefaultSize()
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
    
        switch (specMode) {
            //不考虑UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
            //所以一般情况下,尺寸就是测量的值,由此可知,如果我们继承自View的一个自定义View,在测量的时候,我们需要重写onMeasure(如果是AT_MOST,那么我们就根据情况,提供一个数值,设置为宽高),因为如果是WARP_CONTENT,那么在获取尺寸的时候,就会拿到specSize,而由于MODE是AT_MOST,所以这个尺寸是父容器中可用的最大尺寸,所以效果跟使用MATCH_PARENT一样了。
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    

    ViewGroup的测量

    ViewGroup本身就是一个View,所以他也一样,measure中调用onMeasure,具体的测量逻辑是在onMeasure中,但是ViewGroup是一个抽象类,他的不同的子类,测量规则都没法统一,所以他没有实现onMeasure,在线性布局、相对布局等中都有实现,稍后分析。

    在ViewGroup中有个measureChildren方法,这可以说是一个默认的方法,在我们自己实现onMeasure时可以调用它,也可以不调用(一般测量孩子都调用他),相当于一个模板。看看代码。

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

    很简单,遍历孩子,调用measureChild,内部再让孩子去measure,于是就到了View的测量

    Layout

    layout方法用来决定View自身的位置,在layout中调用了onLayout方法,这个方法没有具体的实现,需要子类自己实现,主要是为了决定子View的位置

    public void layout(int l, int t, int r, int b) {
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
      //设置自身的位置
        boolean changed = setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
            if (ViewDebug.TRACE_HIERARCHY) {
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
            }
            //调用onLayout,具体的实现都不一样
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~LAYOUT_REQUIRED;
    
            if (mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
    }
    

    在Linearlayout中,onLayout中主要就是遍历孩子,然后调用setChildFrame方法,这个方法内部就是调用child的layout方法,所以又回到了上面那一步。

    在调用setChildFrame时,会传入宽高,最终到了child.layout,这个宽高就是通过getMeasureWidth获取的,也就是说,onLayout中的宽高就是测量宽高,所以说,一般情况下,测量宽高和最终的宽高一样,但是测量宽高先被赋值。而且测量宽高可能被多次赋值,所以有时候和最终宽高不一样。

    另一种情况就是layout参数传进来的宽高没有直接使用,比如如下,也会导致测量和最终不同。

    public void layout(int l, int t, int r, int b) {
        super.layout(l,t,r+10,b+10)
    }
    

    Draw

    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
       
        // 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);
    
            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);
    
            // we're done...
            return;
        }
    
       
        // Step 2, save the canvas' layers
       
        // 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
       
    
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }
    

    看注释可知,View的draw过程主要有以下几步:

    • 画背景
    • 画内容
    • 画孩子
    • 画装饰

    draw是通过dispatchDraw将绘画分发给孩子的

    有个方法是setWillNotDraw();可以设置当前view不绘制内容,一般继承自ViewGroup,并且确保自身不需要绘制,就设为true,可以优化。默认为false。

    自定义View

    注意事项

    • 直接继承自View或者ViewGroup需要处理padding和wrap_parent
    • 尽量不要使用handler,因为view自带post
    • 利用onDetachedFromWindow和onAttachedToWindow,维护线程、动画的起止

    自定义View实例

    继承自View的自定义View

    • 在onMeasure中处理wrap_parent
    • 在onDraw中处理padding
    • 自定义xml属性,文件名字不一定要交attrs。自定义属性获取完数据之后记得调用recycle。

    继承自ViewGroup的自定义View

    • 在onMeasure中调用measureChildren测量孩子(也可以自己写逻辑),然后分析自己的measurespec,最后调用setMeasuredDimension
    • onLayout中根据测量宽高,遍历孩子,为其布局。

    相关文章

      网友评论

          本文标题:Android开发艺术(4)——View的工作原理

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