View的工作原理——基本知识(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sliverbullets/article/details/81668634

ViewRoot和DecorView

ViewRoot

ViewRoot可能比较陌生,但是其作用非常重大。所有View的绘制以及事件分发等交互都是通过它来执行或传递的。
ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {

public final class ViewRoot extends Handler implements ViewParent,
 View.AttachInfo.Callbacks {

下面图片为了方便笔记直接拿来用了。来自:图片来源
这里写图片描述

ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。
RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。
并且在追踪ViewRoot时我发现在ActivityThread中的handleResumeActivity()方法中有这么一段:

// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
//翻译:通常ViewRoot使用addView->ViewRootImpl#setView中的活动设置回调。
//如果我们不重用decor视图,我们必须通知视图根,回调可能已经改变。
 ViewRootImpl impl = decor.getViewRootImpl();
 if (impl != null) {
     impl.notifyChildRebuilt();
 }

可以看出来,decor可以获取(创建)ViewRootImpl对象,并且在用impl对象进行通知根视图来做改变(建立关联)。
Veiw的绘制流程是ViewRoot的performTraversals方法开始,它经过measure、layout、draw三个过程才最终把一个View绘制出来。
这里写图片描述
更多介绍请看这篇ViewRootImpl源码分析事件分发

DecorView

它是顶级View,他本身是一个FrameLayout,一般情况下内部会包含一个竖直方向的LinearLayout,在这个LinearLayout中包含上下两个部分,上面是标题栏titlebar,下面是内容栏,id为content。这里就可以理解我们在Activity里面设置View时的方法:setContentView。它是将布局加载到id为content的FrameLayout中。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
如何获取这个content呢?
ViewGroup content =(ViewGroup)findViewById(android.R.id.content)。
ViewGroup rootView = (ViewGroup) content.getChildAt(0);

更多参考链接

MeasureSpec(我这里就简写为MS)

作用:(“测量规格”,“测量说明书”)

它参与了View的测量过程,并且它很大程度的决定了一个View的尺寸,注意View的尺寸还受他父容器的影响,因为父容器影响View的MS的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MS,然后再根据这个MS来测量出View的宽/高。这里测量的宽/高不一定等于最终的宽/高。

介绍

MS代表一个32位的int值,高2位表示SpecMode,SpecMode是指测量模式;低30位代表SpecSize,SpecSize是指在某种测量模式下的规格大小。
这里写图片描述

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 {}

/**
 * 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;

从上面代码可以看出,SpecMode和SpecSize也是int值,是MS为避免过多的对象内存分配,将SpecMode和SpecSize打包成一个MS(就这个),并且MS提供解包方法来得到原始的这两个值。
注意:这里提到的MS,是值MS所代表的int值并非MS本身。

SpecMode有三类中(下面的解释和上面源码的英文意思差不多):
①UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
②EXACTLY: 父容器已经测量出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
③AT_MOST: 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现。它对应于LayoutParams中的warp_content。

MeasureSpec和LayoutParams的对应关系

上面提到的,系统内部是通过MS来进行View的测量,但在正常情况下我们用View指定的MS,尽管如此,但是我们可以给View设置LayoutParams。因为在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MS,然后再根据这个MS来确定View测量后的宽/高。注意:MS不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定MS,从而进一步决定View的宽/高。

顶级View和普通View的MS转换略有不同
- DecorView(顶级View):MS是由窗口的大小和其自身的LayoutParams共同决定。
- 普通View:MS是由其父容器的MS和自身的LayoutParams共同决定,MS一旦确定后,onMeasure中就可以确定View的测量宽/高。

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);    //MS表示宽的
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //MS表示高的  
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

它展示了DecorView的MS的创建过程,其中desirWindowWidth 和 desiredWindowHeight是屏幕的尺寸。
再来看一下getRootMeasureSpec:

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.            
                break;        
    }        
                return measureSpec;    
}

根据上述代码,DecorView的MS产生过程就跟明确了,其遵守如下规则:

  • LayoutParams.MATCH_PARENT: 精确模式,大小就是窗口大小
  • LayoutParams.WRAP_CONTNET:最大模式,大小不定,但是不能超出窗口大小。
  • 固定大小(比如100dp):精准模式,大小为LayoutParams指定的大小。

    对于普通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,并且在调用子元素measure方法前会先获取子元素的MS,从getChildMeasureSpec这个方法可以看出,子元素的MS与父容器的MS,padding,margin,还有子元素的LayoutParams有关。

具体看一下getChildMeasureSpec这个方法是怎么处理这些因素然后转化成子元素的MS的:

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

代码虽然有些长,但不难理解,首先把父容器的MS解包,出来一个specMode和一个specSize。然后传入specMode通过一个规则(switch语句)计算出子元素的specSize和specMode,然后通过最后一句“return MeasureSpec.makeMeasureSpec(resultSize, resultMode)”返回子元素的MS。这里面主要是根据父容器的MS和自身的LayoutParams来确定子元素的MS,参数中Padding是指父容器中已用的空间,所以子元素的大小为父容器的尺寸减去Padding.

下表为代码的总结:
这里写图片描述
验证:对于普通View,其MS是由父容器的MS和自身的LayoutParams决定。
①当View采用固定的宽高时,View的MS是精确模式,大小为LayoutParams设置的。
②当View的宽/高是warp_content时,View的MS是最大模式,大小为父容器的剩余空间大小。
③当View的宽/高是match_parent时,如果父容器是精确模式,则View的MS是精确模式,大小为父容器的剩余部分;如果父容器是最大模式,则View的MS是最大模式,大小为父容器剩余部分。
④还有UNSPECIFIED模式,但这个模式主要用于系统内部多次measure的情形,故一般不关注。所以,只要提供父容器的MS和子元素的LayoutParams就能确定子元素的MS,然后确定子元素的大小。

猜你喜欢

转载自blog.csdn.net/sliverbullets/article/details/81668634