本文借鉴我们伟大的爱哥作品
From AigeStudio(http://blog.csdn.net/aigestudio)
官方说明:
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的作用就先略过了了解即可)。