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

本文详细解析了Android中View和ViewGroup的绘制流程,包括测量、布局和绘制三个核心阶段。阐述了MeasureSpec的作用,以及如何在onMeasure、onLayout和onDraw方法中实现自定义控件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、View的绘制流程

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

  1. 对于一个普通View(不是容器)
    主要是关心测量和绘制两个过程,测量可以确定自身的宽、高、大小,绘制可以显示出view的具体内容(呈现在屏幕上的)。
  2. 对于ViewGroup(容器控件)主要是关心测量和布局两个过程,测量不仅仅要测量自身还要测量所有的子view,布局主要是指定所有子view在自身上的位置。
  3. 具体实现是重写onMeasure、onLayout、onDraw方法,在这些方法中进行编码处理。

二、View的测量

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

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

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

    当父容器的SpecMode为EXACTLY时:
           如果view的LayoutParams为match_parent和具体的值,父容器会为其指定为EXACTLY模式。
           如果view的LayoutParams为wrap_content,父容器会为其指定为AT_MOST模式。
    当父容器的SpecMode为AT_MOST时:
           如果view的LayoutParams为具体的数值,父容器会为其指定为EXACTLY模式。
           如果view的LayoutParams为match_parent,父容器会为其指定为AT_MOST模式。
           如果view的LayoutParams为wrap_content,父容器会为其指定为AT_MOST模式。

    父view在测量子view之前会先调用getChildMeasureSpec方法确定子view的MeasureSpec,下面看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);
      
      //对于viewGroup来说,resultSize和resultMode除了用于确定自身大小外,还要传给下级子view
      //这个变量存的是最终子view测量大小
      int resultSize = 0;
      //这个变量存的是最终子view的测量模式,如果这个子view是viewGroup的话,那么这个测量模式是要给子view的下级子view使用的,就这样一层一层的递归
      int resultMode = 0;
      
      //根据specMode确定子view的MeasureSpec
      switch (specMode) {
       case MeasureSpec.EXACTLY://父容器的模式是EXACTLY,说明父容器的大小是精确值(父容器的大小已经确定了)
           if (childDimension >= 0) {
               //子view的LayoutParams的值是具体的值,比如100dp,那么子view的大小就用这个100dp,子view的模式也是精确值模式
               resultSize = childDimension;
               resultMode = MeasureSpec.EXACTLY;
           } else if (childDimension == LayoutParams.MATCH_PARENT) {
               //子view的LayoutParams的值是MATCH_PARENT,父view是精确值,所以子view的大小就是父view的可用大小(也是精确值),子view的模式也是精确值模式
               resultSize = size;
               resultMode = MeasureSpec.EXACTLY;
           } else if (childDimension == LayoutParams.WRAP_CONTENT) {
               //子view的LayoutParams的值是WRAP_CONTENT,父view是精确值,但是子view自己的大小是不确定的(最大为父view的可用size),所以子view的模式是最大模                     式
               resultSize = size;
               resultMode = MeasureSpec.AT_MOST;
           }
           break;
     
       case MeasureSpec.AT_MOST: //父容器的模式是AT_MOST,说明父容器的大小是不确定的(父容器的大小是一个最大值,这个最大值是父容器的上层父容器给的)
           if (childDimension >= 0) {
               //子view的LayoutParams的值是具体的值,比如100dp,那么子view的大小就用这个100dp,子view的模式也是精确值模式
               resultSize = childDimension;
               resultMode = MeasureSpec.EXACTLY;
           } else if (childDimension == LayoutParams.MATCH_PARENT) {      
               //子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) {
               //子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
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  }           

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

  1. 示例代码实现如下:
  @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);

        //宽或者高的SpecMode是AT_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里使用canvas、paint绘制图形来实现自定义效果。
  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);
   }

  1. onMeasure示例代码实现如下:
 /**
  * 模拟水平方向可滑动的LinearLayout的测量过程,这里不考虑padding和margin的影响
  *
  * @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);
     }
 }
  1. 容器控件的布局,主要是指定每一个子view在自身上的位置,重写onLayout方法,代码实现如下:
  /**
   * 对子view进行布局,这里不考虑padding和margin的影响
   *
   * @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的,实际开发中有很多需求是不需要重头自己定义一个控件的,可以继承一个现有控件,去重写其特定的某一个方法来扩展功能。具体用哪种方式去实现,要具体情况具体分析了,如何选取一种最适合的自定义方式,是值得思考的,也是一个难点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值