Android——View的工作原理(二)

上篇由于篇幅的原因未介绍View的工作流程,想要了解关于 MeasureSpec、LayoutParams、DecorView、ViewRoot相关的请看我的上一篇博客:Android——View的工作原理(一)

主要介绍内容:

  • View的工作流程

    • measure 过程
    • layout 过程
    • drow 过程
  • 自定义View

    • 自定义 View 的分类
    • 自定义 View 条件条件
    • 自定义 View 示例
    • 自定义 View 的思想

View的工作流程

View的工作流程主要是指 measure、layout 、draw 这三大流程,即测量、布局 和 绘制,其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高 和四个顶点的位置,而 draw 则将 View 绘制到屏幕上。

measure 过程

measure 过程要分情况来看,如果只是一个原始的 View,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个流程,下面针对这两种情况分别讨论。

  • 1、View 的 measure 过程

View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法,这意味着子类不能重写此方法,在 View 的 measure 方法中会去调用 VIew 的 onMeasure 方法,因此我们只需要看 onMeasure 方法的实现即可,View 的 onMeasure 方法如下所示。

  //设置该View本身地大小  
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
      setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
              getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  }  

上述代码很简洁,但是简洁不代表简单,setMeasuredDimension 方法会设置View 的宽/高的测量值,隐藏我们只需要看 getDefaultSize 这个方法即可;

 /** 
   * Utility to return a default size. Uses the supplied size if the 
   * MeasureSpec imposed no contraints. Will get larger if allowed 
   * by the MeasureSpec. 
   * 
   * @param size Default size for this view 
   * @param measureSpec Constraints imposed by the parent 
   * @return The size this view should be. 
   */  
  //@param size参数一般表示设置了android:minHeight属性或者该View背景图片的大小值  
  public static int getDefaultSize(int size, int measureSpec) {  
      int result = size;    
      int specMode = MeasureSpec.getMode(measureSpec);  
      int specSize =  MeasureSpec.getSize(measureSpec);  

      //根据不同的mode值,取得宽和高的实际值。  
      switch (specMode) {  
      case MeasureSpec.UNSPECIFIED:  //表示该View的大小父视图未定,设置为默认值  
          result = size;  
          break;  
      case MeasureSpec.AT_MOST:      //表示该View的大小由父视图指定了  
      case MeasureSpec.EXACTLY:  
          result = specSize;  
          break;  
      }  
      return result;  
  } 

可以看出,getDefaultSize 这个方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY 这两种情况。简单地理解,其实 getDefaultSize 返回的大小就是 MeasureSpec 中的 SpecSize,而这个 SpecSize 就是 View 测量后的大小,这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,所以这里必须要加以区别,但是几乎所有情况下 View 的测量大小和最终大小是相等的。

至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下, View 的大小为 getDefaultSize 的第一个参数 size,即宽/高分别为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,看一下它们的源码:

protected int getSuggestedMinimumWidth() {  
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());  
}  

protected int getSuggestedMinimumHeight() {  
    return (mBackground == null) ? mMinHeight : max(mMinHeight , mBackground.getMinimumHeight());  
}  

这里只分析 getSuggestedMinimumWidth 方法的实现,getSuggestedMinimumHeight 和它的实现原理是一样的,从 getSuggestedMinimumWidth 的代码中可以看出,如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值,因此 View 的宽度即为 android:minWidth 属性所指定的值。这个属性如果未指定,那么 mMinWidth 则为默认值 0;如果 View 指定了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth())。mMinWidth 的含义我们已经知道了,那么 mBackground.getMinimumWidth() 是什么呢?我们来看一下 Drawable 的 getMinimumWidth 方法,如下所示:

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

可以看出,getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回 0。那么 Drawable 在什么情况下有原始宽度呢?这里先举个例子说明一下,ShapeDrawable 无原始宽/高,而 BitmapDrawable 有原始宽/高(图片的尺寸),关于 Drawable 相关的知识,我们会在后面另开一篇单独进行介绍,敬请期待…

这里在总结一下 getSuggestedMinimumWidth 的逻辑:如果 View 没有设置背景,那么 返回 android:minWidth 这个属性所设置的值,这个值可以为 0;如果 View 设置了背景,则返回 android:minWidth 和 背景的最小宽度这两者的最大值,getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽/高。

说了这么多,我们好像把我们的老大 setMeasuredDimension 方法给忘了,我们来看一下吧:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
    // 省去部分代码……  

    // 设置测量后的宽高  
    mMeasuredWidth = measuredWidth;  
    mMeasuredHeight = measuredHeight;  

    // 重新将已测量标识位存入mPrivateFlags标识测量的完成  
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
}

所做的操作非常简单,仅仅是保存了我们提供的宽/高的size,重设 MEASURED_DIMENSION_SET 标记位。

从 getDefaultSize 方法的实现来看, View 的宽/高由 SpecSize 决定,所以我们可以得出如下结论:直接继承在 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。为什么呢?这个原因需要结合上述代码以及下表才能更好地理解。从上述代码中我们知道,View 的 MeasureSpec 是由父容器的 MeasureSpec 和 子元素的 LayoutParams 共同决定的,而当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化模式,View 的模式总是最大化模式并且大小不能超过父容器的剩余空间。因此在这里当我们自定义的 View 为 wrap_content,那么根据咱们前面的总结,View 的 SpecSize 肯定为父容器目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。这里其实是引用了上篇文章中的一些总结,不明白的可以点击这里去瞅下上篇博客中提到的 MeasureSpec 和 LayoutParams 的对应关系部分:Android——View的工作原理(一)

这里写图片描述
如何解决这个问题呢?代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ffff00"
    tools:context="com.layoutinflate.mk.www.sticklayout.MainActivity">
    <com.layoutinflate.mk.www.sticklayout.ui.DefaultView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000"/>
</LinearLayout>



    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = 300;
        }
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = 300;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

如图:

这里写图片描述

在上面的代码中,我们只需要给 View 指定一个默认的内部宽/高(300 和 300),并在 wrap_content 时设置此宽/高即可。对于非 wrap_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何设置,这个没有固定的依据,根据需要灵活指定即可。如果查看 TextView、ImageView 等的源码就可以知道,针对 wrap_content 情形,它们的 onMeasure 方法均做了特殊处理,有兴趣的可自行查看它们的源码。

  • 2、ViewGroup的 measure 过程

对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是, ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个叫 measureChildren 的方法,如下所示:

//widthMeasureSpec 和  heightMeasureSpec 表示该父View的布局要求  
//遍历每个子View,然后调用measureChild()方法去实现每个子View大小  
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) { // 不处于 “GONE” 状态  
            measureChild(child, widthMeasureSpec, heightMeasureSpec);  
        }  
    }  
}  

从上述代码来看, ViewGroup 在 measure 时,会对每一个子元素进行 measure ,measureChild 这个方法的实现也很好理解,如下所示:

//测量每个子View高宽时,清楚了该View本身的边距大小,即android:padding属性 或android:paddingLeft等属性标记  
protected void measureChild(View child, int parentWidthMeasureSpec,  
        int parentHeightMeasureSpec) {  
    final LayoutParams lp = child.getLayoutParams(); // LayoutParams属性  
    //设置子View的childWidthMeasureSpec属性,去除了该父View的边距值  mPaddingLeft + mPaddingRight  
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
            mPaddingLeft + mPaddingRight, lp.width);  
    //设置子View的childHeightMeasureSpec属性,去除了该父View的边距值  mPaddingTop + mPaddingBottom  
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
            mPaddingTop + mPaddingBottom, lp.height);  

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
}  

很显然,measureChild 的思想就是取出子元素的 LayoutParams,然后在通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。getChildMeasureSpec 的工作过程我们已经在上一篇博客中详细介绍过: 时光穿梭机

我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要哥哥子类去具体实现,比如 LinearLayout、RelativeLayout等,为什么 ViewGroup 不像 View 一样对其 onMeasure 方法做统一的实现呢? 那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法做统一实现。下面就通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,其他 Layout 类型大家可自行进行分析。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

上述代码很简单,我们选择一个来看一下,比如选择查看竖直布局的 LinearLayout 的测量过程,即 measureVertical 方法,measureVertical 的源码比较长,下面只描述其大概逻辑,首先看一段代码:

//垂直方向布局  
   void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {  
       mTotalLength = 0;         //该LinearLayout测量子View时的总高度。  
    float totalWeight = 0;    //所有子View的权重和 , android:layout_weight  
    int maxWidth = 0;         //保存子View中最大width值  
       ...  
       final int count = getVirtualChildCount();  //子View的个数  

       final int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
       final int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
          ...  
       // See how tall everyone is. Also remember max width.  
       for (int i = 0; i < count; ++i) {  
           final View child = getVirtualChildAt(i);  
              ...  
           LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();  

           totalWeight += lp.weight;    
           //满足该条件地View会在该LinearLayout有剩余高度时,才真正调用measure()  
           if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {  
               ...  
           } else {  
               int oldHeight = Integer.MIN_VALUE;  
               //如果View的hight值为0,并且设置了android:layout_weight属性,重新纠正其height值为WRAP_CONTENT  
               if (lp.height == 0 && lp.weight > 0) {  
                   oldHeight = 0;  
                   lp.height = LayoutParams.WRAP_CONTENT;  
               }  
               // Determine how big this child would like to be. If this or  
               // previous children have given a weight, then we allow it to  
               // use all available space (and we will shrink things later  
               // if needed).  
               //对每个子View调用measure()方法  
               measureChildBeforeLayout(  
                      child, i, widthMeasureSpec, 0, heightMeasureSpec,  
                      totalWeight == 0 ? mTotalLength : 0);  

               //这三行代码做了如下两件事情:  
               //1、获得该View的measuredHeight值,每个View都会根据他们地属性正确设置值  > 0 ;  
               //2、更新mTotalLength值:取当前高度mTotalLength值与mTotalLength + childHeight 的最大值  
               // 于是对于android:layout_height="wrap_height"属性地LinearLayout控件也就知道了它的确切高度值了。  
               final int childHeight = child.getMeasuredHeight();  
               final int totalLength = mTotalLength;
               //这个变量用来存储 LinearLayout 在竖直方向的初始高度。每测量一个子元素,mTotalLength 就会相应的增加,增加部分主要包括子元素的高度以及子元素在竖直方向上的margin等  
               mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +  
                      lp.bottomMargin + getNextLocationOffset(child));  
               ...  
           }  
           final int margin = lp.leftMargin + lp.rightMargin;  
           final int measuredWidth = child.getMeasuredWidth() + margin;  
           maxWidth = Math.max(maxWidth, measuredWidth);  
           ...  
       }  
          //后续还有很多处理,包括继续measure()某些符合条件地子View  
       ...  
   }  


void measureChildBeforeLayout(View child, int childIndex,  
           int widthMeasureSpec, int totalWidth, int heightMeasureSpec,  
           int totalHeight) {  
    //调用measureChildWithMargins()方法去设置子View大小  
       measureChildWithMargins(child, widthMeasureSpec, totalWidth,  
               heightMeasureSpec, totalHeight);  
   } 


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

从 上面这段代码可以看出,系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,这样各个子元素就开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初始高度。每测量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的 margin 等。等子元素测量完毕后,LinearLayout 会测量自己的大小,源码如下所示。

 // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
....
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

这里对上述代码进行说明,当子元素测量完毕后,LinearLayout 会根据子元素的情况来测量自己的大小。针对竖直的 LinearLayout 而言,它在水平方向的测量过程遵循 View 的测量过程,在竖直方向的测量过程则和 View 有所不同。具体来说是指,如果它的布局中高度采用的是 match_parent 或者具体数值,那么它的测量过程和 View 一致,即高度为 SpecSize;如果它的布局中高度采用的是 wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,当然它的最终高度还需要考虑其在竖直方向上的 padding,这个过程可以进一步参看如下源码:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

View 的 measure 过程是三大流程中最复杂的一个,measure 完成以后,通过 getMeasuredWidth/Height 方法就可以正确地获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情况下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。

上面已经对 View 的 measure 过程进行了详细的分析,现在考虑一种情况,比如我们想在 Activity 已启动的时候就做一件任务,但是这一任务需要获取某个 View 的宽/高。大家可能会很简单,在 onCreate 或者 onResume 里面去获取这个 View 的宽/高不就可以了?大家可自行尝试,实际上在 onCreate、onStart、onResume 中均无法正确得到某个 View 的宽/高信息,这是因为 View 的 measure 过程 和 Activity 的生命周期方法不是同步执行的,因此无法保证 Activity 执行了 onCreate、onStart、onResume 时某个 View 已经测量完毕了,如果 View 还没有测量完毕,那么获得的宽/高就是 0,有没有什么方法能解决这个问题呢?答案是有的,这里会给出四种方法来解决这个问题:

  • (1)、Activity/View#onWindowFocusChanged

onWindowFocusChanged 这个方法的含义是:View 已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没有问题的。需要注意的是,onWindowFocusChanged会被调用多次,当 Activity 的窗口得到焦点 和 失去焦点时均会被调用一次。具体来说,当 Activity 继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行 onResume 和 onPause,那么 onWindowFocusChanged 方法也会被频繁地调用。典型代码如下:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        Log.e("onWindowFocusChanged","onWindowFocusChanged");
        if (hasFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }
  • (2)、view.post(runnable)

通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了。典型代码如下:

    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                Log.e("==========","onStart======="+view.getMeasuredWidth()+"======="+view.getMeasuredHeight());
            }
        });
    }
  • (3)、ViewTreeObserver

使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 OnGlobalLayoutListener 这个接口,当 View 数的状态发生改变或者 View 数内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调,因此这是获取 View 的宽/高一个很好的时机。需要注意的是,伴随着 View 树的状态改变等,onGlobalLayout 会被调用多次。典型代码如下:

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                Log.e("==========","onGlobalLayout======="+view.getMeasuredWidth()+"======="+view.getMeasuredHeight());
            }
        });
    }
  • (4)、view.measure(int widthMeasureSpec, int heightMeasureSpec);

通过手动对 View 进行 measure 来得到 View 的宽/高。这种方法比较复杂,这里要分情况处理,根据 View 的 LayoutParams 来分:

  • match_parent

直接放弃,无法 measure 出具体的宽/高,根据 View 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,即 父容器的剩余空间,而这个时候我们无法知道 parentSize 的大小,不知道 parentSize 的大小就无法通过 MeasureSpec.makeMeasureSpec(size, mode);方法构造出 MeasureSpec,也就不可能测量出 View 的大小。
- 具体的数值(dp/px)

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        view.measure(widthMeasureSpec, heightMeasureSpec);

这里需要注意的是 当我们在使用 MeasureSpec.makeMeasureSpec 生成 MeasureSpec 时,如果我们指定的 SpecMode 为 MeasureSpec.EXACTLY 那么当我们在后面的代码中获取 View 的 measureWidth 和 measureHeight 时,measureWidth 和 measureHeight 的大小就是我们设置的 SpecSize 的大小。

  • wrap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        view.measure(widthMeasureSpec, heightMeasureSpec);

这里需要注意的是 当我们在使用 MeasureSpec.makeMeasureSpec 生成 MeasureSpec 时,如果我们指定的 SpecMode 为 MeasureSpec.AT_MOST那么当我们在后面的代码中获取 View 的 measureWidth 和 measureHeight 时,measureWidth 和 measureHeight 的大小是我们在 XML 中为 View 设置的大小 android:layout_width 和 android:layout_height:。

Layout 过程

Layout 的作用是 ViewGroup 用来确定确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 又会调用子元素的 onLayout 方法。Layout 过程相比就简单多了,layout 方法确定 View 本身的位置, 而 onLayout 方法则会确定所以子元素的位置,先看 View 的layout方法,如下所示:

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.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 &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化mLeft、mRight、mTop 和 mBottom 这四个位置,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法,接下来,我们可以看一下 LinearLayout 的 onLayout 方法,如下所示:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

LinearLayout 中 onLayout 的实现逻辑和 onMeasure 的实现逻辑类似,这里选择 layoutVertical 继续介绍,为了更好地理解其逻辑,这里只给出了主要代码:

  @Override  //layout 过程  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
        //假定是垂直方向布局  
        if (mOrientation == VERTICAL) {  
            layoutVertical();  
        } else {  
            layoutHorizontal();  
        }  
    }  
    //对每个子View调用layout过程  
    void layoutVertical() {  
        ...  
        final int count = getVirtualChildCount();  
        ...  
        for (int i = 0; i < count; i++) {  
            final View child = getVirtualChildAt(i);  
            if (child == null) {  //一般为非null  
                childTop += measureNullChild(i);  
            } else if (child.getVisibility() != GONE) {  
                //获得子View测量时的实际宽高值,  
                final int childWidth = child.getMeasuredWidth();  
                final int childHeight = child.getMeasuredHeight();  

                ...  
                //  封装了child.layout()方法,见如下  
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),  
                        childWidth, childHeight);   
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);  

                i += getChildrenSkipCount(child, i);  
            }  
        }  
    } 

    //width = getMeasuredWidth() ; height = childHeight(); View的大小就是测量大小  
    private void setChildFrame(View child, int left, int top, int width, int height) {  

        child.layout(left, top, left + width, top + height);  
    }  

这里分析一下 layoutVertical 的代码逻辑,可以看到,此方法会遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符号竖直方向的 LinearLayout 的特性。至于 setChildFrame,它仅仅是调用子元素的 layout 方法而已,这样父元素在 layout 方法中完成了自己的定位以后,就通过 onLayout 方法去调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样一层一层地传递下去就完成了整个 View 数的layout 过程。

我们注意到,setChildFrame 中的 width 和 height 实际上就是子元素的测量宽/高,从下面的代码可以看出这一点:

//获得子View测量时的实际宽高值,  
                final int childWidth = child.getMeasuredWidth();  
                final int childHeight = child.getMeasuredHeight(); 
 //  封装了child.layout()方法,见如下  
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),  
                        childWidth, childHeight);

而在 layout 方法中会通过 setFrame 去设置子元素的四个顶点的位置,在 setFrame 有如下几句赋值语句,这样一来子元素的位置就确定了;

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

下面我们来回答一下咱们之前提到过的一个问题:View 的测量宽/高和最终宽/高有什么区别?这个问题可以具体为: View 的 getMeasuredWidth 和 getWidth 这两个方法有什么区别,至于 getmeasuredHeight 和 getHeight 的区别和前两者完全一样。为了回答这个问题,首先我们看一下 getWidth 和 getHeight 这两个方法的具体实现:

    public final int getWidth() {
        return mRight - mLeft;
    }
    public final int getHeight() {
        return mBottom - mTop;
    }

从 getWidth 和 getHeight 的源码在结合 mLeft、mRight、mTop 和 mBottom 这四个变量的赋值过程来看,getWidth 方法的返回值刚好就是 View 的测量宽度,而 getHeight 方法的返回值也刚好就是 View 的测量高度。经过上述分析,现在我们可以回答这个问题了:在 View 的默认实现中,View 的测量宽/高 和 最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为 View 的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,下面举例说明。

如果重写 View 的 layout 方法,代码如下:

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

上述代码会导致在任何情况下 View 的最终宽/高总是比测量宽/高大100px,虽然这样做会导致 View 显示不正常并且也没有实际意义,但是这证明了测量宽/高的确可以不等于最终宽/高。另外一种情况是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和 最终宽/高相同。

draw过程

Draw 过程就比较简单了,它的作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

  • (1)、绘制背景 background.draw(canvas)。
  • (2)、绘制自己(onDraw)。
  • (3)、绘制 children(dispatchDraw)。
  • (4)、绘制装饰(onDrawScrollBars)。

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

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

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

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }
....

View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 事件就一层层地传递下去,View 有一个特殊的方法 setWillNotDraw,先看一下它的源码,如下所示:

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

从 setWillNotDraw 这个方法的注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化。默认情况下,View 没有启用这个标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容是,我们需要显式地关闭 WILL_NOT_DRAW 这个标记位。

好了,今天就到这里,后面我们会介绍一下自定义 View 的分类以及注意事项,彻底步入自定义 View 的大门….

呼~ 终于搞完了,累的不要不要的……

相关博文推荐:

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值