自定义View基础——View的测量与流程基础

我们都知道,自定义View的三大流程:Measure、Layout、Draw。而学习这三个流程是我们进阶高级UI的毕竟之路,但是在学习在三个流程之前,一些基础知识是必不可少的,否则只会导致我们越学越难。所以在学习Measure流程之前我们需要先弄清楚这个过程中几个最重要的知识点

一、LayoutParams到底是什么

1、LayoutParams的定义

LayoutParams顾名思义,就是布局参数。而且大多数人对此都是司空见惯,我们 XML 文件里面的每一个 View 都会接触到 layout_xxx 这样的属性,这实际上就是对布局参数的描述。大概大家也就清楚了,layout_ 这样开头的东西都不属于 View,而是控制具体显示在哪里。

2、LayoutParams的组成

LayoutParams的组成是哪些呢?我们先来看看LayoutParams的源码:

//负责给 XML 处理
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

//直接让用户指定宽高
public LayoutParams(int width, int height) {
    this.width = width;
    this.height = height;
}
//供addView这样的方法添加子控件使用
public LayoutParams(LayoutParams source) {
    this.width = source.width;
    this.height = source.height;
}

//供MarginLayoutParams处理
LayoutParams() {  }

实际上,ViewGroup 的子类的 LayoutParams 类拥有更多的构造方法,在这里我想更加强调一下我上面提到的 MarginLayoutParams。MarginLayoutParams 继承于 ViewGroup.LayoutParams

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    @ViewDebug.ExportedProperty(category = "layout")
    public int leftMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    public int topMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    public int rightMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    public int bottomMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    private int startMargin = DEFAULT_MARGIN_RELATIVE;

    @ViewDebug.ExportedProperty(category = "layout")
    private int endMargin = DEFAULT_MARGIN_RELATIVE;

        public MarginLayoutParams(Context c, AttributeSet attrs) {
            super();

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_MarginLayout_layout_width,
                    R.styleable.ViewGroup_MarginLayout_layout_height);

            int margin = a.getDimensionPixelSize(
                    com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
            if (margin >= 0) {
        /*看到这里大家就应该明白了,为什么我们在XML里面设置layout_margin
         *属性的值会覆盖 layout_marginLeft 与 layout_marginRight 等属性的值。
         */
                leftMargin = margin;
                topMargin = margin;
                rightMargin= margin;
                bottomMargin = margin;
            } else {
                int horizontalMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
                int verticalMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);

                if (horizontalMargin >= 0) {
                    leftMargin = horizontalMargin;
                    rightMargin = horizontalMargin;
                } else {
                    leftMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                            UNDEFINED_MARGIN);
                    if (leftMargin == UNDEFINED_MARGIN) {
                        mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                        leftMargin = DEFAULT_MARGIN_RESOLVED;
                    }
                    rightMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                            UNDEFINED_MARGIN);
                    if (rightMargin == UNDEFINED_MARGIN) {
                        mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                        rightMargin = DEFAULT_MARGIN_RESOLVED;
                    }
                }

                startMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                        DEFAULT_MARGIN_RELATIVE);
                endMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                        DEFAULT_MARGIN_RELATIVE);

                if (verticalMargin >= 0) {
                    topMargin = verticalMargin;
                    bottomMargin = verticalMargin;
                } else {
                    topMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                            DEFAULT_MARGIN_RESOLVED);
                    bottomMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                            DEFAULT_MARGIN_RESOLVED);
                }
......
        }
}

看了源码LayoutParams的组成也就一目了然了。with、height 是决定View大小的根本。而MarginLayoutParams 中的margin与padding决定View的内外边距,同样会影响View的最终显示大小。

3、LayoutParams是如何与View建立联系的

LayoutParams建立联系的方式可分为两种:

  • XML中定义View
  • 通过代码addView添加控件

XML中定义View设置LayoutParams属性我们每天都在使用,而这种通过XML设置的属性,最终会通过LayoutInflater的inflate方法进行xml解析,源码分析如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
        省略....
                    if (root != null) {
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    rInflateChildren(parser, temp, attrs, true);
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
省略...
                }
            } 
            return result;
        }
    }

会先调用父窗口的generatLayoutParams方法获取MarginLayoutParams ,然后再addView的时候 把LayoutParams传进去。

代码addView添加控件,少了解析xml的流程,我们看看 addView() 都做了什么。

public void addView(View child) {
    addView(child, -1);
}

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
//获取View的布局参数LayoutParams
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    if (mOrientation == HORIZONTAL) {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    } else if (mOrientation == VERTICAL) {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    return null;
}

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {

    // ...

    if (!checkLayoutParams(params)) {//判断布局参数有效性
        params = generateLayoutParams(params);// 如果传入的LayoutParams不合法,将进行转化操作
    }
 if (preventRequestLayout) { //  是否需要阻止重新执行布局流程
        child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
    }
    // ...
}

  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return  p != null;
    }

二、MeasureSpec类到底是什么

1、MeasureSpec的定义

Android中MeasureSpec的正确定义是:测量规格类。可以将他理解为是测量View大小的一个依据。它的作用是决定一个View的的大小(宽/高),当然了,决定一个View的具体大小并不只是依据这一个参数,还依据每个View的LayoutParams参数。这个下面会具体说明。

2、MeasureSpec的组成

  • 测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)
  • 测量规格(MeasureSpec):32位、int类型
  • 测量模式(mode):占MeasureSpec的高2位
  • 测量大小(size):占MeasureSpec的低30位

(1)、SpecMode有哪些类型

测量模式(Mode)的类型有3种:

UNSPECIFIED:无限模式,父视图不约束子视图View

EXACTLY:精确模式

AT_MOST:最大模式

具体说明如下:

具体的计算方式,下面我们会根据源码来进行分析。

(2)、为什么要SpecMode+SpecSize的组合这样设计MeasureSpec

MeasureSpec类 用1个变量封装了2个数据(size,mode):通过使用二进制,将测量模式(mode) & 测量大小(size)打包成一个int值来,并提供了打包 & 解包的方法。

该措施的目的 = 减少对象内存分配

3、MeasureSpec的计算

(1)MeasureSpec的使用

// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)

// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)

// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

(2)MeasureSpec源码分析

public class MeasureSpec {
  // 进位大小 = 2的30次方
  // int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
  private static final int MODE_SHIFT = 30;  
    
  // 运算遮罩:0x3为16进制,10进制为3,二进制为11
  // 3向左进位30 = 11 00000000000(11后跟30个0)  
  // 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
  private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

  // UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
  // 通过高2位
  public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  
  // EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
  public static final int EXACTLY = 1 << MODE_SHIFT;  

  // AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
  public static final int AT_MOST = 2 << MODE_SHIFT;  

  /**
    * makeMeasureSpec()方法
    * 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
    **/ 
      public static int makeMeasureSpec(int size, int mode) {  
      
          return size + mode;  
      // measureSpec = size + mode;此为二进制的加法 而不是十进制
      // 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
      // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  

      }  

  /**
    * getMode()方法
    * 作用:通过measureSpec获得测量模式(mode)
    **/    

      public static int getMode(int measureSpec) {  
       
          return (measureSpec & MODE_MASK);  
          // 即:测量模式(mode) = measureSpec & MODE_MASK;  
          // MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
          //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
          // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值

      }  
  /**
    * getSize方法
    * 作用:通过measureSpec获得测量大小size
    **/       
      public static int getSize(int measureSpec) {  
       
          return (measureSpec & ~MODE_MASK);  
          // size = measureSpec & ~MODE_MASK;  
         // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size  
      } 
} 

(3)、上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?

答:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,即:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定。如下图:

 

具体计算逻辑封装在getChildMeasureSpec()里,如下源码分析:

/**
* 源码分析:getChildMeasureSpec()
* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
**/

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

 //参数说明
 * @param spec 父view的详细测量值(MeasureSpec) 
 * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
 * @param childDimension 子视图的布局参数(宽/高)

    //父view的测量模式
    int specMode = MeasureSpec.getMode(spec);     

    //父view的大小
    int specSize = MeasureSpec.getSize(spec);     
  
    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);  
  
    //子view想要的实际大小和模式(需要计算)  
    int resultSize = 0;  
    int resultMode = 0;  
  
    //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  


    // 当父view的模式为EXACITY时,父view强加给子view确切的值
   //一般是父view设置为match_parent或者固定值的ViewGroup 
    switch (specMode) {  
    case MeasureSpec.EXACTLY:  
        // 当子view的LayoutParams>0,即有确切的值  比如"layout_width" = "100dp"
        if (childDimension >= 0) {  
            //子view大小为子自身所赋的值,模式大小为EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为MATCH_PARENT时(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小为父view大小,模式为EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)。 也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
    case MeasureSpec.AT_MOST:  
    //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
//child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小,但同样的,child的尺寸上限也是父控件的尺寸上限size
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
//child想要根据自己逻辑决定大小,那就自己决定呗
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
    // 多见于ListView、GridView 等系统控件
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小为子自身所赋的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子View大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子View大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}  

这样看源码好像有点不是太明了,没关系,总结如下:

以父视图为视角:

 以子View为视角:

三、View树的绘制流程

1、View树的绘制流程是什么

源码分析:

/**
  * 源码分析:ViewRootImpl.performTraversals()
  */
  private void performTraversals() {

        // 1. 执行measure流程
        // 内部会调用performMeasure()
        measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);

        // 2. 执行layout流程
        performLayout(lp, mWidth, mHeight);

        // 3. 执行draw流程
        performDraw();
    }

从上面的performTraversals()可知:View的绘制流程从顶级View(DecorView)ViewGroup开始,一层一层从ViewGroup至子View遍历测绘,即:自上而下遍历、由父视图到子视图、每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身,如下图:

而View的绘制流程无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。

  • 绘制的流程 = measure过程、layout过程、draw过程,具体如下

源码流程分析如下:

2、View的绘制由谁来负责

(1)、measure流程

(2)、layout流程

(3)、draw流程

 

发布了29 篇原创文章 · 获赞 3 · 访问量 885

猜你喜欢

转载自blog.csdn.net/LVEfrist/article/details/103563301