概述
View的post方法我一般用来在Activity的onCreate方法中获取View的尺寸,那么为什么在这里面能够正常获取到,它的执行时机又是什么时候,今天来分析一下。
首先把自定义View添加到布局文件中
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.appclient.MainActivity"
android:orientation="vertical"
>
<com.example.appclient.CustomView
android:id="@+id/custom"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</com.example.appclient.CustomView>
</LinearLayout>
用法如下
customView.post(new Runnable() {
@Override
public void run() {
System.out.println("=====自定义View尺寸: " + customView.getWidth());
}
});
customView是我们自定义的一个View,我们先通过打印log的方式直观的看一下这里run方法执行的时机,代码如下
public class CustomView extends View{
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
System.out.println("============构造函数");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
System.out.println("==========onMeasure");
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
System.out.println("==========onFinishInflate");
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
System.out.println("=================onAttachedToWindow");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
System.out.println("==========onLayout");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
System.out.println("==========onDraw");
}
}
从日志中我们就能够明白,为什么post方法中能够正确获取View的尺寸了,因为此时View已经onLayout完毕了。这里还有一个需要注意的地方,不知道大家注意到了没,我们自定义的View在布局文件宽写的是wrap_content,怎么打印的尺寸是768,这里的768是屏幕的宽度。这里就要涉及到自定义View中的知识了,先说下结论,然后我们在从源码的角度来分析一下
结论:当自定义View的尺寸为wrap_content的时候,它的效果和match_parent一样,等于父View的尺寸。
接下来我们从源码的角度分析一下,父View对子View的测量是从measureChildWithMargins方法开始的,这个方法的主要的作用是根据父View的测量规格和尺寸以及子View的布局参数来测量子View
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//获取子View的布局参数
//根据父View的MeasureSpec,padding,子View的margin,宽度等获取子View的MeasureSpec
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的measure方法,传入MeasureSpec
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我们主要看看getChildMeasureSpec方法
spec:父View的MeasureSpec
padding:父View指定的padding,
childDimension:子View的布局参数
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);//父View的测量模式
int specSize = MeasureSpec.getSize(spec);//父View的尺寸
int size = Math.max(0, specSize - padding);//子View的布局参数是wrap_content时,最终设定给它的尺寸大小
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
从上面的代码可以看出,当子View的尺寸设置为wap_content的时候,它最终的尺寸被赋值了size,而size就是父View的尺寸减去padding,如果没有设置padding的话,那么就用父View的尺寸去获取子View的MeasureSpec,获取到子View的MeasureSpec之后,就调用它的measure方法进行测量,measure又会调用onMeasure方法,实际的测量是在onMeasure方法里的.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasureDimension方法用来保存测量之后的宽高,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;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从这里我们可以看到,当我们给View指定尺寸为wrap_content,也就是测量模式是AT_MOST的时候,效果和EXACTLY一样,View的尺寸是specSize,而这个specSize的值是我们从父View传给子View的MeasureSpec中获取出来的,从上面的代码我们分析出来这个尺寸就是父View的尺寸,所以可以验证我们的结论。
Post方法
回到主题,我们这篇文章的重点是讲View的post方法执行的时机。再次给出日志的打印顺序
从如何判断ListView的某个条目是否滑出了屏幕这篇文章我们知道,当attachInfo为空的时候,我们通过post提交的动作会缓存到一个HandlerActionQueue中,执行的时机是在View的performTraversals方法中,关键代码如下
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals");
host.debug();
}
//省去一些代码
host.dispatchAttachedToWindow(mAttachInfo, 0);
//开始测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//开始布局
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performTraversals方法中会依次进行View的measure,layout,draw,我们可以看到,dispatchAttachedToWindow方法是在performMeasure方法之前执行的,那么为什么执行的时机却是onLayout之后呢?
我们的App都是基于消息驱动机制来运行的,主线程的Looper会不断的循环,从MessageQueue中取出消息来执行,只有上一个Message执行完了才会取出下一个Message进行执行,而Handler是用于把Message发送到MessageQueue中去,等轮到Message执行时,会交给Message的target,也就是对应的Handler去执行,执行完了,又取出下一个消息,如此循环。
performTraversals会先执行dispatchAttachedToWindow,
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
//省去一些代码
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
executeActions方法会执行我们通过post方法提交的action,它的过程是这样的,通过info的mHandler对象把消息发送到消息队列去排队执行,而主线程的Handler现在执行的是performMeasure,performLayout等这个消息,等这个消息执行完,就执行我们post提交的消息,这个时候就能够获取尺寸了。