首页 > 代码库 > 流式布局的实现-2-onMeasure的实现

流式布局的实现-2-onMeasure的实现

在这一篇中,我们主要是来实现FlowLayout中的onMeasure函数。

先说一说onMeasure,可以说重载 onMeasure(),onLayout(),onDraw()三个函数构建了自定义View的外观形象。再加上onTouchEvent()等重载视图的行为,可以构建任何我们需要的可感知到的自定义View。我们知道,不管是自定义View还是系统提供的TextView这些,它们都必须放置在 LinearLayout等一些ViewGroup中,因此理论上我们可以很好的理解onMeasure(),onLayout(),onDraw()这 三个函数:1.View本身大小多少,这由onMeasure()决定;2.View在ViewGroup中的位置如何,这由onLayout()决 定;3.绘制View,onDraw()定义了如何绘制这个View。

在TextView中onMeasure是:

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);
  
         int width;
         int height;
  
         ... 
         if (widthMode == MeasureSpec.EXACTLY) {  
           // Parent has told us how big to be. So be it.
             width = widthSize;          
         } 
         else {
           if (mLayout != null && mEllipsize == null) { 
                      des = desired(mLayout);             
            }  
                      ...    
         }       
         setMeasuredDimension(width, height);  
    }

首先我们要理解的是widthMeasureSpec, heightMeasureSpec这两个参数是从哪里来的?onMeasure()函数由包含这个View的具体的ViewGroup调用,因此值也是 从这个ViewGroup中传入的。这里我直接给出答案:子类View的这两个参数,由ViewGroup中的 layout_width,layout_height和padding以及View自身的layout_margin共同决定。权值weight也是尤其需要考虑的因素,有它的存在情况可能会稍微复杂点。

了解了这两个参数的来源,还要知道这两个值的作用。我们只取 heightMeasureSpec作说明。这个值由高32位和低16位组成,高32位保存的值叫specMode,可以通过如代码中所示的 MeasureSpec.getMode()获取;低16位为specSize,同样可以由MeasureSpec.getSize()获取。那么 specMode和specSize的作用有是什么呢?要想知道这一点,我们需要知道代码中的最后一行,所有的View的onMeasure()的最后一行都会调用setMeasureDimension()函数的作用——这个函数调用中传进去的值是View最终的视图大小。也就是说 onMeasure()中之前所作的所有工作都是为了最后这一句话服务的。

之后我们开始一片一片分析代码:

第一部分:
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

分别获得在widthMeasureSpec,heightMeasureSpec中的size与mode:

getSize,getMode和过程中涉及到的变量:

public static int getMode(int measureSpec) {
      return (measureSpec & MODE_MASK);
 }
public static int getSize(int measureSpec) {
       return (measureSpec & ~MODE_MASK);
 } 
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;
第二部分:
        int width = 0;
        int height = 0;
 
        // 记录每一行的宽度与高度
        int lineWidth = 0;
        int lineHeight = 0;
 
        // 得到内部元素的个数
        int cCount = getChildCount();
在注释中都已说明,就不解释了;
第三部分:
  for (int i = 0; i < cCount; i++)
        {
            View child = getChildAt(i);
            // 测量子View的宽和高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 得到LayoutParams
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();
 
            // 子View占据的宽度
            int childWidth = child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            // 子View占据的高度
            int childHeight = child.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;
 
            // 换行
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight())
            {
                // 对比得到最大的宽度
                width = Math.max(width, lineWidth);
                // 重置lineWidth
                lineWidth = childWidth;
                // 记录行高
                height += lineHeight;
                lineHeight = childHeight;
            } else
            // 未换行
            {
                // 叠加行宽
                lineWidth += childWidth;
                // 得到当前行最大的高度
                lineHeight = Math.max(lineHeight, childHeight);
            }
            // 最后一个控件
            if (i == cCount - 1)
            {
                width = Math.max(lineWidth, width);
                height += lineHeight;
            }
        }

先获得子View,再获得子View的layoutParams,因为是有间距的,所以注意要子View的LayoutParams强制转化为MarginLayoutParams;

重新计算子View的宽:因为有左右间距,所以新的宽是原宽加上左右间距;同理,高也是如此;

之后判断要不要换行,即已计算的行宽加上新的子View的宽,如果大于FlowLayout的宽减去左右边距说明剩余的宽度,不够加上新的子View,要把新的子View放到新的一行中去(其实有做过acm的话,可以用到线段树来进行优化),否则加入到原行中。之后在这两种情况下对行高,行宽,高和宽做不同的调整;

如果到了最后一个子View的话:如果要换行的话,高只是记录到之前的部分,而对换行后新加的高度即最后一个子View的高没有加上,宽的话,如果最后一个子View的宽很大超过之前的话,也没有更新;如果不要换行的话,最后一行的高与宽就始终没有加上;所以最后一个View要进行特判;因为无论是换行还是不换行,行宽都更新过了,所以只要和最大的宽做比较即可;高则只要加上行高即可;

第四部分:
  setMeasuredDimension(
                //
                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop()+ getPaddingBottom()//
        );
判断是不是MeasureSpec.EXACTLY即是不是内容铺满父容器,是的话,就直接将一开始调用getSize得到的宽高传入即可,如果不是,则要将测得的宽高加上边距传入到setMeasuredDimension函数中,来确定View最终的视图大小。


流式布局的实现-2-onMeasure的实现