自定义控件深度探索-控件的测量

本文借鉴我们伟大的爱哥作品

From AigeStudio(http://blog.csdn.net/aigestudio)

一、View结构原理
Android系统的视图结构的设计也采用了组合模式,即View作为所有图形的基类,Viewgroup对View继承扩展为视图容器类。 View定义了绘图的基本操作 基本操作由三个函数完成:measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法。
1、measure操作
     measure操作主要用于计算视图的大小,即视图的宽度和长度。在view中定义为final类型,要求子类不能修改。measure()函数中又会调用下面的函数:
     (1)onMeasure(),视图大小的将在这里最终确定,也就是说measure只是对onMeasure的一个包装,子类可以覆写onMeasure()方法实现自己的计算视图大小的方式,并通过setMeasuredDimension(width, height)保存计算结果。
 
2、layout操作
     layout操作用于设置视图在屏幕中显示的位置。在view中定义为final类型,要求子类不能修改。layout()函数中有两个基本操作:
     (1)setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来;
     (2)onLayout(),在View中这个函数什么都不会做,提供该函数主要是为viewGroup类型布局子视图用的;
 
3、draw操作
     draw操作利用前两部得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。子类也不应该修改该方法,因为其内部定义了绘图的基本操作:
     (1)绘制背景;
     (2)如果要视图显示渐变框,这里会做一些准备工作;
     (3)绘制视图本身,即调用onDraw()函数。在view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的显示(比如TextView在这里实现了绘制文字的过程)。而对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的,其包含了多个子view,而子View已经实现了自己的绘制方法,因此只需要告诉子view绘制自己就可以了,也就是下面的dispatchDraw()方法;
     (4)绘制子视图,即dispatchDraw()函数。在view中这是个空函数,具体的视图不需要实现该方法,它是专门为容器类准备的,也就是容器类必须实现该方法;
     (5)如果需要(应用程序调用了setVerticalFadingEdge或者setHorizontalFadingEdge),开始绘制渐变框;
     (6)绘制滚动条;
      从上面可以看出自定义View需要最少覆写onMeasure()和onDraw()两个方法。

官方说明:

onDraw()和onMeasure()

onDraw()函数将会传给你一个 Canvas 对象,通过它你可以在二维图形上做任何事情,包括其他的一些标准和通用的组件、文本的格式,任何你可以想到的东西都可以通过它实现。

注意: 这里不包括三维图形如果你想使用三维的图形,你应该把你的父类由View改为SurfaceView类,并且用一个单独的线程。可以参考GLSurfaceViewActivity 的例子。

onMeasure() 函数有点棘手,因为这个函数是体现组件和容器交互的关键部分,onMeasure()应该重载,让它能够有效而准确的表现它所包含部分的测量值。这就有点复杂了,因为我们不但要考虑父类的限制(通过onMeasure()传过来的),同时我们应该知道一旦测量宽度和高度出来后,就要立即调用setMeasuredDimension() 方法。

概括的来讲,执行onMeasure()函数分为一下几个阶段:

1.      重载的onMeasure()方法会被调用,高度和宽度参数同时也会涉及到(widthMeasureSpec 和heighMeasureSpec两个参数都是整数类型),同时你应该考虑你产品的尺寸限制。这里详细的内容可以参考View.onMeasure(int, int) (这个连接内容详细的解释了整个measurement操作)。

2.      你的组件要通过onMeasure()计算得到必要的measurement长度和宽度从而来显示你的组件,它应该与规格保持一致,尽管它可以实现一些规格以外的功能(在这个例子里,父类能够选择做什么,包括剪切、滑动、提交异常或者用不同的参数又一次调用onMeasure()函数)。

3.      一旦高度和宽度计算出来之后,必须调用setMeasuredDimension(int width, int height),否则就会导致异常。

1.理解Activity根结构框架


@Override
protected void onMeasure(<strong>int widthMeasureSpec, int heightMeasureSpec</strong>) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

问题:自定义控件使用默认onMeasure方法后,他的兄弟控件,都被遮挡,无论设置matchparent或者wrapcontent都没用这是为啥子?

原因:因为默认的onMeasure方法会调用getDefaultSize方法内部由于measureSpec的Mode为At_Most所以默认将父控件最大size赋值给自定义View,父控件的宽高为MatchParent。

默认情况下onMeasure方法中只是简单地将签名列表中的两个int型参数回传给父类的onMeasure方法,然后由父类的方法去计算出最终的测量值。但是,这里有个问题非常重要,就是onMeasure签名列表中的这两个参数是从何而来,这里可以告诉大家的是,这两个参数是由view的父容器,代码中也就是我们的LinearLayout传递进来的,很多初学Android的朋友会将位于xml布局文件顶端的控件称之为根布局,比如这里我们的LinearLayout,而事实上在Android的GUI框架中,这个LinearLayout还称不上根布局,它从何而来???

说了大半天才理清这个小关系,但是我们还没说到重点…………………………就是widthMeasureSpec和heightMeasureSpec究竟是从哪来的……………………如果我们不做上面的一个分析,很多童鞋压根无从下手,有了上面一个分析,我们知道我们界面的真正根视图应该是DecorView,那么我们的widthMeasureSpec和heightMeasureSpec应该从这里或者更上一层PhoneWindow传递进来对吧,但是DecorView是FrameLayout的一个实例,在FrameLayout的onMeasure中我们确实有对子元素的测量,但是问题是FrameLayout:onMeasure方法中的widthMeasureSpec和heightMeasureSpec又是从何而来呢?在Android中这一功能由ViewRootImpl承担,我们在前面提到过这个类,其负责的东西很多,比如我们窗口的显示、用户的输入输出当然还有关于处理我们绘制流程的方法:

private void performTraversals() {
	// ………省略宇宙尘埃数量那么多的代码………

	if (!mStopped) {
		// ……省略一些代码

		int childWidthMeasureSpec = <strong>getRootMeasureSpec</strong>(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

        // ……省省省

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	}

	// ………省略人体细胞数量那么多的代码………
}
这里我们重点来看getRootMeasureSpec方法是如何确定测量规格的,首先我们要知道mWidth, lp.width和mHeight, lp.height这两组参数的意义,其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定,mWidth和mHeight表示当前窗口的大小,其值由performTraversals中一系列逻辑计算确定,这里跳过,而在getRootMeasureSpec中作了如下判断:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int <strong>measureSpec</strong>;
    switch (rootDimension) {

    case <strong>ViewGroup.LayoutParams.MATCH_PARENT:
    	// Window不能调整其大小,强制使根视图大小与Window一致
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, <span style="color:#ff0000;">MeasureSpec.EXACTLY</span>);</strong>
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
    	// Window可以调整其大小,为根视图设置一个最大值
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
    	// Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

至此,我们算是真正接触到根视图的测量规格(),尔后这个规格会被由上至下传递下去,并由当前view与其父容器共同作用决定最终的测量大小,在View与ViewGroup递归调用实现测量的过程中有几个重要的方法,对于View而言则是measure方法:也就是说不管如何,我们的根视图大小必定都是全屏的……

我们并没有对其做任何的处理,也就是说保持了其在父类View中的默认实现,其默认实现也很简单:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其直接调用了setMeasuredDimension方法为其设置了两个计算后的测量值:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
	// 省去部分代码……

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

    // 重新将已测量标识位存入mPrivateFlags标识测量的完成
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
回到onMeasure方法,我们来看看这两个测量值具体是怎么获得的,其实非常简单,首先来看getSuggestedMinimumWidth方法:
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果背景为空那么我们直接返回mMinWidth最小宽度否则就在mMinWidth和背景最小宽度之间取一个最大值,getSuggestedMinimumHeight类同,mMinWidth和mMinHeight我没记错的话应该都是100px,而getDefaultSize方法呢也很简单:

public static int getDefaultSize(int size, int measureSpec) {
	// 将我们获得的最小值赋给result
    int result = size;

	// 从measureSpec中解算出测量规格的模式和尺寸
    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;
}

注意上述代码中当模式为(父控件LinearLayout设置默认为AT_MOST)AT_MOST以及EXACTLY时均会返回解算出的测量尺寸,还记得上面我们说的PhoneWindow、DecorView么从它们那里获取到的测量规格(measureSpec)层层传递到我们的自定义View中(MatchParent后的屏幕宽高)这就是为什么我们的View在默认情况下不管是设置math_parent还是warp_content都能占满父容器的剩余空间(这里面还有父布局LinearLayout的作用就先略过了了解即可)








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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值