自定义view流程(结合源码分析)

一、View的绘制流程
   
主要是:测量(measure)、布局(layout)、绘制(draw)三大流程。

    1
、对于一个普通View(不是容器)
   
主要是关心测量和绘制两个过程,测量可以确定自身的宽、高、大小,绘制可以显示出view的具体内容(呈现在屏幕上的)。

    2
、对于ViewGroup(容器控件)主要是关心测量和布局两个过程,测量不仅仅要测量自身还要测量所有的子view,布局主要是指定所有子view在自身上的位置。

    3
、具体实现是重写onMeasureonLayoutonDraw方法,在这些方法中进行编码处理。


二、View的测量
    1
view的测量主要是由自身的MeasureSpec决定的,而自身的MeasureSpec又由父容器的MeasureSpec和自身的LayoutParams决定的。

    2
MeasureSpec包含SpecModeSpecSize两部分,SpecMode是测量规则,SpecSize是在一定规则下的测量大小。

    3
SpecMode分为三种模式
       a
UNAPECIFIED:父容器对view没有任何限制,要多大给多大,该模式多为系统自己使用,自定义一般不考虑该模式。
       b
EXACTLY:父容器已经知道view所需要的精确大小(SpecSize)。
       c
AT_MOST:父容器给了一个最大值(SpecSize)。

      
当父容器的SpecModeEXACTLY时:
          
如果viewLayoutParamsmatch_parent和具体的值,父容器会为其指定为EXACTLY模式。
          
如果viewLayoutParamswrap_content,父容器会为其指定为AT_MOST模式。
      
当父容器的SpecModeAT_MOST时:
          
如果viewLayoutParams为具体的数值,父容器会为其指定为EXACTLY模式。
          
如果viewLayoutParamsmatch_parent,父容器会为其指定为AT_MOST模式。
          
如果viewLayoutParamswrap_content,父容器会为其指定为AT_MOST模式。
        

       父view在测量子view之前会先调用getChildMeasureSpec方法确定子view的MeasureSpec,下面看getChildMeasureSpec的源码:

       getChildMeasureSpec的源码:
   //确定子view的MeasureSpec的具体方法
   public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

    //父view自己的模式和大小
       int specMode = MeasureSpec.getMode(spec);
       int specSize = MeasureSpec.getSize(spec);

       //父view的大小减去padding值,就是现在子view可用的空间大小
       int size = Math.max(0, specSize - padding);

       //这个变量存的是最终子view测量大小
       int resultSize = 0;

       //这个变量存的是最终子view的测量模式,如果这个子view是viewGroup的话,那么这个测量模式是要给子view的下级子view使用的,就这样一层一层的递归
       int resultMode = 0;

       //对于viewGroup来说,resultSize和resultMode除了用于确定自身大小外,还要传给下级子view

       switch (specMode) {
       //父容器的模式是EXACTLY,说明父容器的大小是精确值(父容器的大小已经确定了)
       case MeasureSpec.EXACTLY:
           if (childDimension >= 0) {
               //子view的LayoutParams的值是具体的值,比如100dp,那么子view的大小就用这个100dp,子view的模式也是精确值模式
               resultSize = childDimension;
               resultMode = MeasureSpec.EXACTLY;
           } else if (childDimension == LayoutParams.MATCH_PARENT) {
               // Child wants to be our size. So be it.
               //子view的LayoutParams的值是MATCH_PARENT,父view是精确值,所以子view的大小就是父view的可用大小(也是精确值),子view的模式也是精确值模式
               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.
               //子view的LayoutParams的值是WRAP_CONTENT,父view是精确值,但是子view自己的大小是不确定的(最大为父view的可用size),所以子view的模式是最大模                     式
               resultSize = size;
               resultMode = MeasureSpec.AT_MOST;
           }
           break;
      
       //父容器的模式是AT_MOST,说明父容器的大小是不确定的(父容器的大小是一个最大值,这个最大值是父容器的上层父容器给的)
       case MeasureSpec.AT_MOST:
           if (childDimension >= 0) {
               // Child wants a specific size... so be it
               //子view的LayoutParams的值是具体的值,比如100dp,那么子view的大小就用这个100dp,子view的模式也是精确值模式
               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.
               //子view的LayoutParams的值是MATCH_PARENT,那么子view的大小就是父view的可用大小,而父view的模式是AT_MOST,说明父view的大小是不确定的
               //,所以子view的大小也是不确定的,子view的模式是AT_MOST模式

               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.
               //子view的LayoutParams的值是WRAP_CONTENT,父view的大小不确定,子view自身的大小也不确定,所以子view的模式是AT_MOST模式
               resultSize = size;
               resultMode = MeasureSpec.AT_MOST;
           }
           break;

        //这种模式一般是系统自己用的,自定义控件一般不考虑这种情况
       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;
       }

       //最终根据resultSize和resultMode生成子view的MeasureSpec
       //noinspection ResourceType
       return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
   }           

       综上所述:如果viewSpecModeEXACTLY时,其大小是确定的,不需要做特殊处理。如果viewSpecModeAT_MOST时,其大小是不确定的,所以在测量时需要视                         情况设置一个默认值,否则wrap_content是无效的、不显示内容的。


      4、 示例
代码实现如下:
            @Override
           protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
                super.onMeasure(widthMeasureSpec,heightMeasureSpec);
                //
获取父容器为其指定的测量模式和测量尺寸
                int widthSpecMode =MeasureSpec.getMode(widthMeasureSpec);
                int widthSpecSize =MeasureSpec.getSize(widthMeasureSpec);
                int hightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
                int hightSpecSize =MeasureSpec.getSize(heightMeasureSpec);

                //
宽或者高的SpecModeAT_MOST时就设置一个默认值,如果不是就用SpecSize
                if (widthSpecMode ==MeasureSpec.AT_MOST && hightSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(500,500);
                } else if (widthSpecMode ==MeasureSpec.AT_MOST) {
                    setMeasuredDimension(500,hightSpecSize);
                } else {
                    setMeasuredDimension(widthSpecSize,500);
                }
           }


三、View的绘制
    1
view的绘制是通过重写onDraw方法实现的,可以在onDraw里使用canvaspaint绘制图形来实现自定义效果。

    2
、需要注意的是在绘制的时候需要考虑padding的影响,如果不做处理padding会无效,因为padding是跟view本身有关的。不用关心margin,因为margin是跟父容器相关的,跟view自身无关。

    3
、为了可以方便的在xml中改变效果,还需要对外提供自定义属性。


四、ViewGroup的测量和布局
    1
、viewGroup的onMeasure方法中,既要测量自身的大小,又要测量子view的大小,测量子view的大小可以使用measureChildren测量所有子view,也可以自己写for循环遍             历测量子view,调用的方法是measureChild和measureChildWithMargins(这两个方法是测量单个子view的)。下面对这些方法的源码进行分析:

          measureChildren的源码:
             //测量所有子view
            protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
                        final int size = mChildrenCount;
                        final View[] children = mChildren;
                       //循环测量子view
                       for (int i = 0; i < size; ++i) {
                                  final View child = children[i];
                                 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                                 //执行测量子view的方法
                                 measureChild(child, widthMeasureSpec, heightMeasureSpec);
                       }
                 }
           }

 measureChild的源码:
   //具体测量一个子view的方法
   protected void measureChild(View child, int parentWidthMeasureSpec,
           int parentHeightMeasureSpec) {

       //获取到子view的参数LayoutParams,后面会用
       final LayoutParams lp = child.getLayoutParams();

       //根据LayoutParams里面设置的宽的match_parent或者wrap_content(即lp.width),在结合父view的MeasureSpec来确定子view的宽的MeasureSpec
       final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
               mPaddingLeft + mPaddingRight, lp.width);

       //根据LayoutParams里面设置的高的match_parent或者wrap_content(即lp.height),在结合父view的MeasureSpec来确定子view的高的MeasureSpec
       final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
               mPaddingTop + mPaddingBottom, lp.height);

       //上面的几行代码是确定子view的MeasureSpec的,这行代码是真正进行子view测量的,将上面确定下来的子view的MeasureSpec传给子view。
       child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
   }

     2、 onMeasure示例代码实现如下:
       /**
        *
模拟水平方向可滑动的LinearLayout的测量过程,这里不考虑paddingmargin的影响
        *
        * @param widthMeasureSpec
        * @param heightMeasureSpec
        */
       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
           super.onMeasure(widthMeasureSpec, heightMeasureSpec);

           //
获取父容器为其指定的测量模式和测量尺寸
           int widthMode = MeasureSpec.getMode(widthMeasureSpec);
           int widthSize = MeasureSpec.getSize(widthMeasureSpec);
           int hightMode = MeasureSpec.getMode(heightMeasureSpec);
           int hightSize = MeasureSpec.getSize(heightMeasureSpec);

           //
测量所有子view的宽和高
           measureChildren(widthMeasureSpec, heightMeasureSpec);

           //
测量自身的宽和高
           int measureWidth = 0;
           int measureHight = 0;
           int childCount = getChildCount();
           if (childCount != 0) {
                //
计算出由子view决定的宽度
                for (int i = 0; i < childCount;i++) {
                    measureWidth +=getChildAt(i).getMeasuredWidth();
                }

                //
计算出由子view决定的高度(选取子view中高度最大值为其测量高度)
                for (int j = 0; j < childCount;j++) {
                    if(getChildAt(j).getMeasuredHeight() > measureHight) {
                        measureHight =getChildAt(j).getMeasuredHeight();
                    }
                }
           }

           if (widthMode == MeasureSpec.AT_MOST && hightMode ==MeasureSpec.AT_MOST) {
                setMeasuredDimension(measureWidth,measureHight);
           } else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(measureWidth,hightSize);
           } else if (hightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSize,measureHight);
           }
       }
   
   3
、容器控件的布局,主要是指定每一个子view在自身上的位置,重写onLayout方法,代码实现如下:
        /**
        *
对子view进行布局,这里不考虑paddingmargin的影响
        *
        * @param changed
        * @param left
        * @param top
        * @param right
        * @param bottom
        */
       @Override
       protected void onLayout(boolean changed, int left, int top, int right,int bottom) {

           //
每个子view的左起点
           int childLeft = 0;
           //
view的个数
           int childCount = getChildCount();

           //
为每个子view指定位置
           for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                childView.layout(childLeft, 0,childLeft + childView.getMeasuredWidth(), childView.getMeasuredHeight());
                childLeft +=childView.getMeasuredWidth();
           }
       }


五、在现有控件的基础上进行自定义
   
上面所说的自定义控件,都是直接继承View或者ViewGroup的,实际开发中有很多需求是不需要重头自己定义一个控件的,可以继承一个现有控件,去重写其特定的某一个方法来扩展功能。具体用哪种方式去实现,要具体情况具体分析了,如何选取一种最适合的自定义方式,是值得思考的,也是一个难点。









 

猜你喜欢

转载自blog.csdn.net/huideveloper/article/details/71102251