简述
android的界面显示其实分成两块,一块了系统的DecorView(顶级view)
和普通view ,而DecorView包含一个竖直的LinearLayout,上部分是titleBar ,下部分是一个id为content的frameLayout
,不管是显示过程
还是事件分发
,都是由它传递而来。另一块就是我们自己设置填充到DecorView中framelayout的view 。现在是不是有点体会了,为什么在给activity设置布局的时候方法是setContentView()
,因为我们的确是把view设置到id为content的FrameLayout当中的。
不管是DecorView或者是普通view ,要显示到界面上都要经历三个过程:measure(测量宽高)
、layout(布局位置)
、draw(绘制)
view的测量过程
View的测量过程决定了它的宽高,MeasureSpec
参与了view的测量,所以我们有必要详细了解一下
MeasureSpec
MeasureSpec代表了一个32位的int值,高2位是SpecMode(测量模式)
,低30位是测量SpecSize(测量尺寸)
,而SpecMode又分为三种,不同的模式下最终生成的尺寸是不一样的
AT_MOST (最大模式):父容器指定一个可用的大小即SpecSize,子view的大小不可超过该尺寸,具体视图子view的实现而定,对应于
wrap_content
属性EXACTLY (精准模式):父容器已经计算出了确定的值,子view的最终大小就是SpecSize的值,对应于
match_parent和确定值
UNSPECIFIED (未指定):父容器不对子view做任何限制,需要多大就给多大,一般用于系统内部,我们就不用太过关心了
现在我们大概了解了什么是MeasureSpec,那么它是怎样生成的呢?
对于DecorView,它的MeasureSpec由屏幕尺寸和自身的LayoutParamas
决定。对于普通view,它的measure过程由ViewGroup的measureChild()
调用
protected void measureChild(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
由此可见,子view的measureSpec由父容器的MeasureSpec、padding/margin、自身layoutParamas
决定,具体实现由于代码稍多就不贴出来了,有兴趣可以自己研究。总结起来就是:
子view的宽高是具体值,SpecMode总是EXACTLY,
最后的宽高就是设置的值
子view的宽高为match_parent,则SpecMode(测量模式)由父容器决定,
最后的宽高取决于父容器的测量尺寸
子view的宽高为wrap_content,则SpecMode(测量模式)始终是AT_MOST,
最后的宽高不能超过父容器的剩余空间
View的measure过程
view的measure()
方法是一个final方法,里面调用的是onMeasure()
,如下:
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
setMeasureDimension()
会把view的宽高测量设置进系统,再来看看系统默认的处理测量的宽高的方法getDefaultSize()
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//获取测量模式和测量值
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//这种模式主要用于系统的多次测量,我们不用关心
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//在我们关心的这2中模式下,返回的其实就是测量值
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这个方法的逻辑也很简单,简单来说就是直接返回了测量值(SpecSize)
,当然这是系统默认的处理,我们在自定义view的时候可以重写onMeasure()
方法,根据自己的逻辑把测量值设置进去。
额外补充一点,如果我们自定义view的时候默认系统设置测量值的方法,那么wrap_content
的作用效果跟match_parent
一样,为什么?回看一下上面的总结,当view的宽高属性值为wrap_content
时,测量模式为AT_MOST
,测量尺寸为specSize
,就是父容器的剩余可用尺寸
,这不就跟match_parent
效果一样了么!
那有什么方法能处理么?很简单,重写onMeasure()
,当宽高属性为wrap_content
的时候,给设置一个默认的大小,ok搞定
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽高的测量模式
int withSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int withSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//当宽/高为wrap_content即对用AT_MOST的时候,设置一个默认值给系统
if (withSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(defWith,defHeight);
}else if (withSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(defWith,heightSpecSize);
}else {
setMeasuredDimension(withSpecSize,defHeight);
}
}
ViewGroup的measure过程
对于ViewGroup
本身,系统并没有提供一个默认的onMeasure方法,因为不同特性的ViewGroup
(比如LinearLayout
和RelativeLayout
)他们的测量方式肯定是不一样的,所以需要子类自己去实现。而对于ViewGroup里面的view,则会通过measureChildren()
循环遍历每一个view,然后调用measureChild()
(这个方法的逻辑跟measureChildWithMargins()
一模一样),如此从而完成测量。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild()
方法的核心是:取出子view的layoutParamas
和自身的MeasureSpec
生成MeasureSpec
传递给子view的measure()
方法,完成对子view的测量
获取View的测量宽/高
measure()
方法完成后,可以通过getMeasureHeight/with
获取测量的宽高,但是这时取出的值并不一定是最终的值,某些情况下系统会多次调用measure才能完成测量。所以最准确的方式是在layout
中获取测量的宽高值
如果我们想在activity或者fragment中获取一个view的测量宽高怎么办?
- 在activity中可在onWindowFocusChanged()方法里面获取,缺点就是activity得到或者是去焦点时都会回调,可用导致频繁的调用
@Overridepublic void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
etUsername.getMeasuredWidth();
}
}
- view.post(runnable)这种方法是极力推荐的,投递一个runnable到消息队列,当looper处理这条消息的时候,view也初始化好了
etUsername.post(new Runnable() {
@Override public void run() {
etUsername.getMeasuredWidth();
}
});