继承ViewGroup重写onMeasure方法的详解

简介:

我们继承重写ViewGroup的目的是要做自定义控件,所以我们有必要先看一下安卓View的绘制过程:

  首先当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点。

  绘制过程从布局的根节点开始,从根节点开始测量和绘制整个layout tree,绘画通过遍历整个树来完成,不可见的区域的View被放弃。

  每一个ViewGroup 负责要求它的每一个孩子被绘制,每一个View负责绘制自己。

  因为整个树是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。

  

  绘制是两个过程:一个measure过程和一个layout过程。


  1.测量过程

是在measure(int, int)中实现的,是从树的顶端由上到下进行的。

在这个递归过程中,每一个View会把自己的dimension specifications传递下去。

在measure过程的最后,每一个View都存储好了自己的measurements,即测量结果。

 

  2.布局过程 

发生在 layout(int, int, int, int)中,仍然是从上到下进行。

   在这一遍中,每一个parent都会负责用测量过程中得到的尺寸,把自己的所有孩子放在正确的地方。


所以在继承ViewGroup类时,需要重写两个方法,分别是onMeasureonLayout。重写ViewGroup的过程大致是两个:


1)测量过程>>>onMeasure(int widthMeasureSpec, int heightMeasureSpec)

传入的参数是本View的可见长和宽,通过这个方法循环测量所有View的尺寸并且存储在View里面;


2)布局过程>>>onLayout(boolean changed, int l, int t, int r, int b)

传入的参数是View可见区域的上下左右四边的位置,在这个方法里面可以通过layout来放置子View;


我们先来看一下测量的过程,也就是该如何重写onMeasure方法,重写之前我们先要了解这个方法:


onMeasure方法

  onMeasure方法是测量view和它的内容,决定measured width和measured height的,子类可以覆写onMeasure来提供更加准确和有效的测量。

  注意:在覆写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。

  如果没有这么做,将会由measure(int, int)方法抛出一个IllegalStateException

     并且覆写onMeasure方法的时候,子类有责任确保measured height and width至少为这个View的最小height和width。getSuggestedMinimumHeight() and getSuggestedMinimumWidth()

  

  onMeasure方法如下:

 

protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)

其中两个参数如下:

widthMeasureSpec

heightMeasureSpec

传入的参数是两个int分别是parent提出的水平和垂直的空间要求


这两个要求是按照View.MeasureSpec类来进行编码的。参见View.MeasureSpec这个类的说明:


两个参数分别代表宽度和高度的MeasureSpec,android2.2文档中对于MeasureSpec中的说明是:


 一个MeasureSpec封装了从父容器传递给子容器的布局需求.每一个MeasureSpec代表了一个宽度,或者高度的说明.一个MeasureSpec是一个大小跟模式的组合值.

  这个类包装了从parent传递下来的布局要求,传递给这个child。

  简单地说就是每一个MeasureSpec代表了对宽度或者高度的一个要求。

  每一个MeasureSpec有一个尺寸(size)和一个模式(mode)构成。

  MeasureSpecs这个类提供了把一个<size, mode>的元组包装进一个int型的方法,从而减少对象分配。当然也提供了逆向的解析方法,从int值中解出size和mode。我们先看三种模式:

 

  有三种模式:

  UNSPECIFIED

  这说明parent没有对child强加任何限制,child可以是它想要的任何尺寸,子容器想要多大就多大

  EXACTLY

  Parent为child决定了一个绝对尺寸,child将会被赋予这些边界限制,不管child自己想要多大,子容器应当服从这些边界


  AT_MOST

  Child可以是自己任意的大小,但是有个绝对尺寸的上限,即子容器可以是声明大小内的任意大小


当我们设置width或height为fill_parent时,容器在布局时调用子view的measure方法传入的模式是EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的。而当设置为wrap_content时,容器传进去的是AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸。当子view的大小设置为精确值时,容器传入的是EXACTLY, 而MeasureSpec的UNSPECIFIED模式目前还没有发现在什么情况下使用。

 View的onMeasure方法默认行为是当模式为UNSPECIFIED时,设置尺寸为mMinWidth(通常为0)或者背景drawable的最小尺寸,当模式为EXACTLY或者AT_MOST时,尺寸设置为传入的MeasureSpec的大小。


具体取出模式或者值的方法:


根据提供的测量值(格式)提取模式(上述三个模式之一)

int widthMode = MeasureSpec.getMode(widthMeasureSpec); 

int heightMode = MeasureSpec.getMode(heightMeasureSpec); 


根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)

int widthSize = MeasureSpec.getSize(widthMeasureSpec); 

int heightSize = MeasureSpec.getSize(heightMeasureSpec); 


而合成则可以使用下面的方法:

根据提供的大小值和模式创建一个测量值(格式)

MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);

我们回忆一下之前一开始讲过的View绘制过程

  当一个View的measure()方法返回的时候,它的getMeasuredWidth和getMeasuredHeight方法的值一定是被设置好的。它所有的子节点同样被设置好。一个View的测量宽和测量高一定要遵循父View的约束,这保证了在测量过程结束的时候,所有的父View可以接受子View的测量值。一个父View或许会多次调用子View的measure()方法。举个例子,父View会使用不明确的尺寸去丈量看看子View到底需要多大,当子View总的尺寸太大或者太小的时候会再次使用实际的尺寸去调用onMeasure().

 

下面我们来看看具体代码:

我们来看View类中measure和onMeasure函数的源码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || 
                widthMeasureSpec != mOldWidthMeasureSpec || 
                heightMeasureSpec != mOldHeightMeasureSpec) { 
 
            // first clears the measured dimension flag 
            mPrivateFlags &= ~MEASURED_DIMENSION_SET; 
 
            if (ViewDebug.TRACE_HIERARCHY) { 
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); 
            } 
 
            // measure ourselves, this should set the measured dimension flag back 
            onMeasure(widthMeasureSpec, heightMeasureSpec); 
 
            // flag not set, setMeasuredDimension() was not invoked, we raise 
            // an exception to warn the developer 
            if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { 
                throw new IllegalStateException("onMeasure() did not set the" 
                        + " measured dimension by calling" 
                        + " setMeasuredDimension()"); 
            } 
 
            mPrivateFlags |= LAYOUT_REQUIRED; 
        } 
 
        mOldWidthMeasureSpec = widthMeasureSpec; 
        mOldHeightMeasureSpec = heightMeasureSpec; 
    } 


measure的过程是固定的,而measure中调用了onMeasure函数,因此真正有变数的是onMeasure函数,onMeasure的默认实现很简单,源码如下:


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
    } 

    onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,而measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值。一旦这两个变量被赋值,则意味着该View的测量工作结束。


protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 
        mMeasuredWidth = measuredWidth; 
        mMeasuredHeight = measuredHeight; 
 
        mPrivateFlags |= MEASURED_DIMENSION_SET; 
    } 

    对于非ViewGroup的View而言,通过调用上面默认的measure——>onMeasure,即可完成View的测量,当然你也可以重载onMeasure,并调用setMeasuredDimension来设置任意大小的布局,但一般不这么做。


    对于ViewGroup的子类而言,往往会重载onMeasure函数负责其children的measure工作,重载时不要忘记调用setMeasuredDimension来设置自身的mMeasuredWidth和mMeasuredHeight。如果我们在layout的时候不需要依赖子视图的大小,那么不重载onMeasure也可以,但是必须重载onLayout来安排子视图的位置。

    ViewGroup中定义了measureChildren, measureChild,  measureChildWithMargins来对子视图进行测量,measureChildren内部只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。


getChildMeasureSpec的总体思路就是通过其父视图提供的MeasureSpec参数得到specMode和specSize,并根据计算出来的specMode以及子视图的childDimension(layout_width和layout_height中定义的)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension,而该函数的参数正是这里计算出来的。


      总结:从上面的描述看出,决定权最大的就是View的设计者,因为设计者可以通过调用setMeasuredDimension决定视图的最终大小,例如调用setMeasuredDimension(100, 100)将视图的mMeasuredWidth和mMeasuredHeight设置为100,100,那么父视图提供的大小以及程序员在xml中设置的layout_width和layout_height将完全不起作用,当然良好的设计一般会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,以尊重程序员的意图。


 下面我们看一下具体的重写代码:


/**
	 * 计算控件的大小
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int measureWidth = measureWidth(0, widthMeasureSpec);
		int measureHeight = measureHeight(0, heightMeasureSpec);
		// 计算自定义的ViewGroup中所有子控件的大小
		// 首先判断params.width的值是多少,有三种情况。
		//
		// 如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。
		//
		// 如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。
		//
		// 如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。
		// measureChildren(widthMeasureSpec, heightMeasureSpec);
		for (int i = 0; i < getChildCount(); i++) {
			View v = getChildAt(i);

			int widthSpec = 0;
			int heightSpec = 0;
			LayoutParams params = v.getLayoutParams();
			if (params.width > 0) {
				widthSpec = MeasureSpec.makeMeasureSpec(params.width,
						MeasureSpec.EXACTLY);
			} else if (params.width == -1) {
				widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,
						MeasureSpec.EXACTLY);
			} else if (params.width == -2) {
				widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,
						MeasureSpec.AT_MOST);
			}

			if (params.height > 0) {
				heightSpec = MeasureSpec.makeMeasureSpec(params.height,
						MeasureSpec.EXACTLY);
			} else if (params.height == -1) {
				heightSpec = MeasureSpec.makeMeasureSpec(measureHeight,
						MeasureSpec.EXACTLY);
			} else if (params.height == -2) {
				heightSpec = MeasureSpec.makeMeasureSpec(measureWidth,
						MeasureSpec.AT_MOST);
			}
			v.measure(widthSpec, heightSpec);

		}
		// 设置自定义的控件MyViewGroup的大小
		setMeasuredDimension(measureWidth, measureHeight);
	}

	private int measureWidth(int size, int pWidthMeasureSpec) {
		int result = size;
		int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式
		int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸

		switch (widthMode) {
		/**
		 * mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY,
		 * MeasureSpec.AT_MOST。
		 * 
		 * 
		 * MeasureSpec.EXACTLY是精确尺寸,
		 * 当我们将控件的layout_width或layout_height指定为具体数值时如andorid
		 * :layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
		 * 
		 * 
		 * MeasureSpec.AT_MOST是最大尺寸,
		 * 当控件的layout_width或layout_height指定为WRAP_CONTENT时
		 * ,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可
		 * 。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
		 * 
		 * 
		 * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,
		 * 通过measure方法传入的模式。
		 */
		case MeasureSpec.AT_MOST:
		case MeasureSpec.EXACTLY:
			result = widthSize;
			break;
		}
		return result;
	}

	private int measureHeight(int size, int pHeightMeasureSpec) {
		int result = size;

		int heightMode = MeasureSpec.getMode(pHeightMeasureSpec);
		int heightSize = MeasureSpec.getSize(pHeightMeasureSpec);

		switch (heightMode) {
		case MeasureSpec.AT_MOST:
		case MeasureSpec.EXACTLY:
			result = heightSize;
			break;
		}
		return result;
	}

这是一个重写的简单例子,已经经过测试了。我再贴一下这个类的代码吧:

package com.example.component;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class MyLayout extends ViewGroup {

// 三种默认构造器
<span style="white-space:pre">	</span>public MyLayout(Context context) {
<span style="white-space:pre">		</span>super(context);
<span style="white-space:pre">	</span>}

	public MyLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public MyLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	/**
	 * 计算控件的大小
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int measureWidth = measureWidth(0, widthMeasureSpec);
		int measureHeight = measureHeight(0, heightMeasureSpec);
		// 计算自定义的ViewGroup中所有子控件的大小
		// 首先判断params.width的值是多少,有三种情况。
		//
		// 如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。
		//
		// 如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。
		//
		// 如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。
		// measureChildren(widthMeasureSpec, heightMeasureSpec);
		for (int i = 0; i < getChildCount(); i++) {
			View v = getChildAt(i);

			int widthSpec = 0;
			int heightSpec = 0;
			LayoutParams params = v.getLayoutParams();
			if (params.width > 0) {
				widthSpec = MeasureSpec.makeMeasureSpec(params.width,
						MeasureSpec.EXACTLY);
			} else if (params.width == -1) {
				widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,
						MeasureSpec.EXACTLY);
			} else if (params.width == -2) {
				widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,
						MeasureSpec.AT_MOST);
			}

			if (params.height > 0) {
				heightSpec = MeasureSpec.makeMeasureSpec(params.height,
						MeasureSpec.EXACTLY);
			} else if (params.height == -1) {
				heightSpec = MeasureSpec.makeMeasureSpec(measureHeight,
						MeasureSpec.EXACTLY);
			} else if (params.height == -2) {
				heightSpec = MeasureSpec.makeMeasureSpec(measureWidth,
						MeasureSpec.AT_MOST);
			}
			v.measure(widthSpec, heightSpec);

		}
		// 设置自定义的控件MyLayout的大小
		setMeasuredDimension(measureWidth, measureHeight);
	}

	private int measureWidth(int size, int pWidthMeasureSpec) {
		int result = size;
		int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式
		int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸

		switch (widthMode) {
		/**
		 * mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY,
		 * MeasureSpec.AT_MOST。
		 * 
		 * 
		 * MeasureSpec.EXACTLY是精确尺寸,
		 * 当我们将控件的layout_width或layout_height指定为具体数值时如andorid
		 * :layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
		 * 
		 * 
		 * MeasureSpec.AT_MOST是最大尺寸,
		 * 当控件的layout_width或layout_height指定为WRAP_CONTENT时
		 * ,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可
		 * 。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
		 * 
		 * 
		 * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,
		 * 通过measure方法传入的模式。
		 */
		case MeasureSpec.AT_MOST:
		case MeasureSpec.EXACTLY:
			result = widthSize;
			break;
		}
		return result;
	}

	private int measureHeight(int size, int pHeightMeasureSpec) {
		int result = size;

		int heightMode = MeasureSpec.getMode(pHeightMeasureSpec);
		int heightSize = MeasureSpec.getSize(pHeightMeasureSpec);

		switch (heightMode) {
		case MeasureSpec.AT_MOST:
		case MeasureSpec.EXACTLY:
			result = heightSize;
			break;
		}
		return result;
	}

	/**
	 * 覆写onLayout,其目的是为了指定视图的显示位置,方法执行的前后顺序是在onMeasure之后,因为视图肯定是只有知道大小的情况下,
	 * 才能确定怎么摆放
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		// 记录总高度
		int mTotalHeight = 0;
		// 遍历所有子视图
		int childCount = getChildCount();
		for (int i = 0; i < childCount; i++) {
			View childView = getChildAt(i);

			// 获取在onMeasure中计算的视图尺寸
			int measureHeight = childView.getMeasuredHeight();
			int measuredWidth = childView.getMeasuredWidth();

			childView.layout(l, mTotalHeight, measuredWidth, mTotalHeight
					+ measureHeight);

			mTotalHeight += measureHeight;

		}
	}
}


希望大家能有所收获。所用到的知识上面已经讲过了,我对这部分知识目前理解的也还是不透彻,最近需要用到,从网上看了很多大神的文章边学边写的,等到我继续深入之后还会再给大家补充。

我也是学生,还请大家多多指教,这次篇幅有些长了,下一次如果时间充裕,我在学习onLayout方法和学习一些例子的时候还会和大家分享的!

目录
相关文章
|
7月前
|
XML Android开发 数据格式
自定义View之重写onMeasure
自定义View之重写onMeasure
68 0
|
XML Android开发 数据格式
进入Activity时,为何页面布局内View#onMeasure会被调用两次?
进入Activity时,为何页面布局内View#onMeasure会被调用两次?
|
Java
java方法重写(Override)
1.重写的概念 重写是子类对父类(或爷爷及以上类)的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写! 重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。 2.实现一个简单的Demo 父类:
127 0
java方法重写(Override)
|
Java
Java面向对象——继承、super、this
Java面向对象——继承、super、this
153 0
Java面向对象——继承、super、this
|
前端开发 Java Android开发
自定义控件View之onMeasure调用时机源码分析
终于建了一个自己个人小站:https://huangtianyu.gitee.io,以后优先更新小站博客,欢迎进站,O(∩_∩)O~~ 先上测试代码: MainActivity.java import android.
1230 0
|
XML Android开发 数据格式
Activity子类详解
前言 往者不可谏,来者犹可追。 建立自己的Activity需要继承Activity基类。当然,在不同的场景下,你也可以继承Activity的子类来简化开发。
1049 0
继承、super、重写
整体与部分(has a关系)例如:球队与球员 继承 注意细节: 1.子类可以继承父类的成员,但是千万不要为了减少重复代码去继承,只有正真存在继承的时候才去继承 2.
640 0
|
Java Android开发 容器
getSupportFragmentManager要用在FragmentActivity及其子类中
getSupportFragmentManager要用在FragmentActivity及其子类中!!   关于安卓抽屉导航!!  * 自定义侧边栏                 创建一个Fragment:CarlozLibFragment.
1246 0