从源码学习自定义View(一):Measure过程

android提供给我们的View很多,但是在开发中我们会遇到各种不同的需求,对View的样式也会有不同的要求。这时系统提供给我们的View控件就不够用了,最好的方法就是使用自定义View,这样的View是可控的,可以根据我们的需求去定制它的样式。
而要自定义View就必须对View整个绘制过程了解透彻,这样才能在自定义View的时候得心应手,而要了解其绘制过程当然是需要从源码追踪的,理解其绘制过程和步骤。

Measure

  我们知道,对于View而言,显示在屏幕上要经历三个步骤,measure,layout和draw。而这个过程,是ViewRootImpl通过依次调用performMeasure/Layout/Draw这三个方法实现的,他们分别对应着View的这三个过程。而这三个步骤也将会从顶层View开始,然后逐步分发到其子View,从而实现整个绘制的过程。

// View.java

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    /* widthMeasureSpec ,heightMeasureSpec
	* 这两个参数是父View传递进来的,该值是根据父View的尺寸和模式然后结合子View的尺寸和模式计算所得
	* 这两个参数是父View所给的建议尺寸,子View应当根据这两个值进行适当的测量处理
	* 对于顶层View,由于没有父View,因此这两个值是根据屏幕大小来生成的。
	* 每一个View测量子View的时候都应当为子View生成该尺寸模式传递下去,而这个尺寸模式实际上可以理解为
	* 约束条件,子View需要准守这个约束条件来进行自己的测量过程
	*/
	boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    // Suppress sign extension for the low bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & ffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);


    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec|| heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    // 根据上面的几种情况判断是否需要重新进行布局   
    final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

	// 当需要重新测量的时候 或者是View调用了requestLayout
    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
			
        resolveRtlPropertiesIfNeeded();

		// 若 不是因为requestLayout而引起的重新布局,则先从缓存中查找
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // 若缓存中没有查询到,则重新测量自身
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
           // 否则的话直接设置为缓存中获取的值
           setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

	// 将测量结果保存在缓存中
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

  我们可以看到,在measure中操作不多,主要做了两件事,测量自身和设置测量结果值。
  在需要测量的时候,会先从缓存中查询结果,若是查询不到的话或者强制布局或者忽略缓存,则会调用onMeasure方法进行测量,否则的话直接通过setMeasuredDimensionRaw将缓存中读取的值设置为测量结果。最后将传递进来的测量参数保存到缓存中取。

  对于MeasureSpec我们应当知道,它内部定义了三种模式,EXACTLY,AT_MOST和UNSPECIFIED,和一系列的将模式和尺寸拆分合成的方法。
  父View给子View传递的measureSpec虽然是一个int值,但它且包含两个部分,高2位的mode和低30位的size。我们可以通过MeasureSpec将他们进行分离组合。

  那么继续看View的onMeasure方法。

// View.java

// 多层嵌套,最终调用setMeasuredDimensionRaw将结果保存下来
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
 
// 保存最终的结果值   
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

  上面分析的是onMeasure内部嵌套的最外层,可以看到的是在View的onMeasure中并没有做什么太多的测量,只是将获取的默认值(这个值是父View所传递进来的,父View通过计算所传递给子View的建议尺寸)通过setMeasuredDimension继而调用setMeasuredDimensionRaw方法进行设置,这就完成了保存测量值的过程。
  另外可以看到在setMeasuredDimensionRaw中,直接将测量的结果值保存在了mMeasureWidth和mMeasureHeight中,此时就可以通过View的getMeasuredWidth和getMeasuredHeight方法来获取View的测量宽高了。但是注意的是,这个宽高并不一定是View的实际大小。实际大小是以布局时的坐标而定的,当然大部分情况下它们是一致的。

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

  对于外层的setMeasured已经分析过了,现在接着看内层的getDefaultSize,这个方法返回的是默认情况下的测量值。它接受两个参数,分别是getSuggestedMinimumWidth()的返回值和父View 传递进来的测量值。

// View.java

/*
* 这个方法是View提供的用于获取默认情况下View的测量宽高的方法
* 该方法没有具体计算View本身应该的宽高,而是直接拿来父View给的建议宽高来直接当作本身的宽高
* 而对于系统方法所提供的UNSPECIFIED模式,则设置自身宽高为minWidth/Height和背景宽高相比的较大值
*/ 
public static int getDefaultSize(int size, int measureSpec) {
    // measureSpec : 父View给的建议宽高
    // size :默认最小值,android:minWidth和背景宽高相比的,该值的获取在后面有说明

	int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

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

  getDefaultSize方法有两个参数,第一个是getSuggestedMinimumWidth()的返回值,第二个是父View给的建议尺寸参数。可以看到,当测量模式是UNSPECIFIED的时候,获取的默认值就是getSuggestedMinimumWidth的值。而在AT_MOST和EXACTLY模式中的值都是一样的,是父View传递的建议值的大小。
  getSuggestedMinimumWidth则会先判断View是否设置了背景,若是没设置,则使用mMinWidth 的值,该值是在布局中的android:minWidth设置的,默认为0。若是设置了背景,则比较该值与背景的宽,取较大的值,高的尺寸也是一样。

  另外可以看出在AT_MOST和EXACTLY模式的时候值是一样的,也就是说在默认情况下,View的宽高就是父View给的建议值。因此,在我们自定义View的时候应该重写onMeasure方法,然后在原测量的基础上处理wrap_content的情况。至于match_parent和具体数值则可以直接使用默认的这种测量情况,因为match_parent的时候父View为子View构建的测量参数就是父View的宽高,具体数值的时候构建的子View的宽高也是该具体数值,这都是满足我们条件的。唯一不满足我们条件的就是wrap_content模式,该情况下父View为子View构建的测量参数也是父View的宽高,因此若是我们不处理的话,就会造成在子View中wrap_content和map_parent一样为填充父布局。而构建子View的测量参数的规则在ViewGroup中,我们稍后会分析。

 到这里就可以看出了View的测量过程:
  1,首先调用measure方法判断是否需要重新测量。
  2,需要重新测量先从缓存中查找,有的话直接设置缓存中的值。
  3,缓存中没有的话则使用onMeasure方法进行测量,然后设置测量值。

  由于View没有子View,所以它的测量比较简单,但是ViewGroup就不一样了(虽然ViewGroup也是View,但是由于它可以包含子View,故这里将它与普通View区分开来),它不仅需要测量自身,还需要测量子View。
  由上面的分析可以知道,measure是final修饰的不能重写的。也就是说View与ViewGroup的测量的过程的区别只在onMeasure中会有所区分。然而在ViewGroup中也是没有重写onMeasure的,也就是说ViewGroup也会和View一样,直接使用父View传递进来的测量参数来设置自己的宽高,这显然是不满足我们的要求的。因为ViewGroup是可能含有子View的,因此对于ViewGroup的测量我们必须考虑到子View的宽高,也就是说在我们测量ViewGroup的时候,我们必须先测量它的子View,然后根据子View的测量宽高再进行自身的测量。
  因此ViewGroup中为我们定义了一些方法来辅助我们进行测量。常用的是measureChildWithMargins。

// ViewGroup.java

// 参数分别是父View的布局参数和已使用过的宽高
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 子View的属性参数
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 设置margin,构建给子View的建议宽高模式参数
    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);
	// 调用子View的测量方法进行测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

  该方法通过 getChildMeasureSpec方法生成给子View的建议测量参数,然后再交给子View去进行自己的测量。注意的是该方法在获取子View的测量参数的时候是去掉了margin和padding的。

// ViewGroup.java

// 参数是父View的spec参数,padding,子View的布局参数的宽高
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 父布局的测量模式和尺寸
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

	// 去掉了padding(在measureChildWithMargins中该值还包括了margin)后的父布局的大小。
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 父布局是精确模式,对应match_parent或者具体值
    case MeasureSpec.EXACTLY:
    	// 子布局是具体值,则大小为子布局的大小,并设为精确模式
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        // 子布局为match_parent,则设置为父布局的大小(size在上面已经计算过),设为精确模式。
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
         // 子布局为wrap_content,则设为父布局大小(该设置的是实际大小最大只能取到这个值),设为最大模式。
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 父布局是最大模式值,对应wrap_content
    case MeasureSpec.AT_MOST:
        // 子布局是具体值,则大小为子布局的大小,并设为精确模式
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        // 子View为match_parent,则设为父布局大小(该设置的是实际大小最大只能取到这个值),设为最大模式。
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        // 子View为wrap_content,设为父布局大小,设为最大模式
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 未指定模式,无限制模式
    case MeasureSpec.UNSPECIFIED:
    	// 子View为具体值,设为该值并设为EXACLTY精确模式
        if (childDimension >= 0) { 
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        // 子View为match_parent,则设为父布局大小,并设为未指定模式UNSPECIFIED
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        // 子View为wrap_content,设为父布局大小,并设为未指定模式UNSPECIFIED
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    // 根据尺寸和模式构建MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

  上面的方法就是生成子View 的建议测量参数的规则,它不单纯是根据父View或者子View来进行构建,而是基于二者共同的影响下进行构建,返回值就是构建出的给子View的建议测量宽高。一直在说建议测量宽高,因为这个值是父View给子View传递的宽高模式,子View将会根据这个参数来设置自己宽高,而建议二字则是说子View并不一定必须根据这两个参数进行设置,只不过大部分情况下子View都要准守的。
子View的测量参数的生成规则
  上述图片就是构建子View测量参数的规则,这是在父View 和子View共同影响下产生的。是从ViewGroup的getMeasureSpec方法中总结出的。

总结规则:
1,尺寸:

  当子View为具体的数值的时候,构建的参数的尺寸就是该数值,其余情况都是父View的尺寸。

2,模式

A:父View非UNSPECIFIED模式下:
  a1,当子View为具体的数值的时候,构建的模式都是EXACTLY
  a2,当子View为match_parent并且父View为EXACTLY的时候,构建的模式是EXACTLY
  a3,其余均是AT_MOST
B:父View为UNSPECIFIED 模式下:
  b1,子View为具体数值的时候模式为EXACTLY
  b2,其余情况为UNSPECIFIED

  上述为生成子View的建议测量参数时的规则,当我们自定义View的时候也应当遵守这种规则。

  此外ViewGroup还有一个方法用来测量子View,与measureChildWithMargins类似,只不过是少测量了margin而已。

//ViewGroup.java

// 循环遍历子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) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
// 测量子View,只计算了padding而没有计算margin
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

  上面的方法measureChild可以看出与measureChildWithMargins是一致的,区别就是measureChild没有测量margin。另外measureChild还提供了一个遍历的方法measureChildren来帮助遍历子View进行测量。而带margin的则没有,需要我们手动遍历。这两个方法都是ViewGroup提供给我们的,我们可以选择合适的方法去调用,但是普遍情况下,对于View而言还是带有margin属性比较友好一些。因此在使用measureChild的时候,我们最好自己计算它的margin然后进行设置。而measureChildren则用的很少,它只适用于不含margin属性的View。

总结:

  自定义View的话布局阶段需要以下几个步骤:

   1,重写onMeasure方法,在onMeasure中实现测量
   2,若是ViewGroup则需要循环遍历子View
    2.1,对子View进行measureChild[WithMargins],该方法会测量子View的宽高并将测量过程传递下去
    2.2,通过子View的getMeasuredWidth/Height获取子View的测量宽高,然后进行测量自己的宽高
   3,测量自己的时候需要处理宽高为wrap_content的情况
   4,将测量结果通过setMeasuredDimension保存
   5,测量结束


相关文章目录:

  从源码学习自定义View(一):Measure过程
  从源码学习自定义View(二):LayoutParams
  从源码学习自定义View(三):Layout和Draw过程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值