Android 开发探秘:View.post()为何能获取View宽高
开发中的小困惑
在 Android 开发的奇妙世界里,获取 View 的宽高是一个常见的操作,但也常常让开发者们感到困惑。你是否曾经遇到过这样的情况:在 Activity 的onCreate方法中,信心满满地调用view.getWidth()或view.getHeight(),满心期待能得到正确的宽高值,结果却得到了 0?这是不是让你感到十分疑惑,甚至有点抓狂呢🤯
例如,我们在布局文件中定义了一个简单的 TextView:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/my_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>
然后在 Activity 的onCreate方法中尝试获取它的宽高:
public class MyActivity extends AppCompatActivity {
private static final String TAG = MyActivity.class.getSimpleName();
private TextView tv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.my_text);
Log.d(TAG, "11111 width: " + tv.getMeasuredWidth() + " - height : " + tv.getHeight());
}
}
运行后你会发现,日志输出的宽高都是 0。这是因为在onCreate方法中,View 还没有完成测量和布局,此时获取宽高自然为 0 。
然而,神奇的是,当我们使用view.post()方法时,却能成功获取到 View 的宽高。就像这样:
public class MyActivity extends AppCompatActivity {
private static final String TAG = MyActivity.class.getSimpleName();
private TextView tv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.my_text);
Log.d(TAG, "11111 width: " + tv.getMeasuredWidth() + " - height : " + tv.getHeight());
tv.post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "22222 width: " + tv.getMeasuredWidth() + " - height : " + tv.getMeasuredHeight());
}
});
}
}
这次运行后,日志中22222对应的宽高就不再是 0 了,而是正确的数值。这到底是为什么呢🧐 为什么view.post()有这样神奇的魔力?接下来,就让我们一起深入探究其中的奥秘。
初窥 View.post ()
()
在 Android 的世界里,View.post()方法就像是一个神秘的小助手。它的作用是将一个Runnable任务投递到主线程的消息队列中去执行 。简单来说,当你调用view.post(Runnable action)时,你传入的Runnable代码块并不会立即执行,而是会被添加到主线程消息队列的末尾,等待主线程在合适的时候(也就是消息队列中前面的任务都执行完后)来执行它。
而它在获取 View 宽高这件事上,有着独特的作用。就像我们前面提到的例子,在onCreate中直接获取宽高失败,但使用view.post()就能成功获取。这就像是它掌握了获取宽高的 “正确时机” 的秘诀,那么这个秘诀到底是什么呢🧐 接下来我们就深入到源码的世界去一探究竟。
常规获取宽高的 “尴尬”
在 Android 开发中,我们常常在 Activity 的onCreate、onStart、onResume等生命周期方法中尝试获取 View 的宽高 。但往往会得到令人失望的 0 值。这是为什么呢🧐
这要从 View 的绘制流程说起。View 的绘制流程主要分为三个阶段:测量(measure)、布局(layout)和绘制(draw)。在测量阶段,系统会计算 View 的宽高,确定measuredWidth和measuredHeight;布局阶段则确定 View 在父容器中的位置;最后绘制阶段将 View 绘制到屏幕上 。
而在onCreate、onStart等方法执行时,View 还处于初始化阶段,远远没有完成测量和布局过程。就好比你正在建造一座房子,房子还只是画了设计图,连地基都还没打,你就问房子有多高多大,这显然是不合理的,因为它还没有成型呢。所以此时调用view.getWidth()和view.getHeight()方法获取宽高,得到的自然是 0 。只有当 View 完成了测量和布局,这些方法才能获取到正确的宽高值 。这也就解释了为什么在常规的生命周期方法中直接获取宽高会失败。
View.post () 神奇揭秘
1. 源码解读
要解开view.post()能获取 View 宽高的谜题,我们得深入到它的源码中去。View.post()方法的源码如下:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
从这段源码中,我们可以看到它首先判断AttachInfo是否为空 。AttachInfo是 View 的一个内部类,它保存了 View 与窗口相关的一些信息,每个 View 都会持有一个AttachInfo,默认情况下它是null 。
如果attachInfo不为空,就直接调用attachInfo.mHandler.post(action)。这里的mHandler是主线程的Handler,这意味着可以将任务直接通过主线程的Handler发送到主线程的消息队列中去执行 。
而当attachInfo为空时,就会调用getRunQueue().post(action)。getRunQueue()方法返回的是一个HandlerActionQueue对象:
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
HandlerActionQueue类的post方法如下:
public void post(Runnable action) {
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
可以看到,它会将传入的Runnable封装成一个HandlerAction对象,并将其添加到一个数组mActions中进行缓存 。简单来说,当attachInfo为空时,View.post()会把任务先缓存起来,等待后续执行。
2. 关键流程追踪
我们知道了attachInfo为空时任务会被缓存,那么这些缓存的任务什么时候会被执行呢🧐 这就涉及到attachInfo的赋值过程了。在 View 的源码中,只有一处给mAttachInfo赋值的地方,是在dispatchAttachedToWindow方法中:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
// 其他代码...
}
当View执行dispatchAttachedToWindow方法时,会给mAttachInfo赋值,并且会执行之前缓存的任务 。而dispatchAttachedToWindow方法是在 View 绘制、测量的时候被调用的 。具体来说,是在ViewRootImpl的performTraversals方法中,会遍历DecorView中的子 View 并执行子 View 的dispatchAttachedToWindow方法 。这就像是一场精心安排的演出,在合适的时机,那些被缓存的任务就会被拿出来执行。
3. 执行时机探究
结合前面提到的 View 绘制流程,我们可以更清楚地理解View.post()获取宽高的原理 。在 Activity 的onCreate、onStart等方法执行时,View 还没有开始绘制,attachInfo为空,View.post()的任务被缓存 。当执行到onResume之后,ViewRootImpl的performTraversals方法被调用,开始进行 View 的测量(measure)和布局(layout) 。在这个过程中,View会执行dispatchAttachedToWindow方法,attachInfo被赋值,之前缓存的View.post()任务被执行 。此时,View 已经完成了测量和布局,所以在View.post()的Runnable中就可以获取到正确的宽高值了 。而且,这些任务的执行是在首帧绘制之前,确保了我们能及时获取到宽高用于后续的操作 。
对比其他获取宽高方法
除了view.post()方法,还有一些其他方法可以获取 View 的宽高 ,它们各有特点和适用场景 。
1. 监听布局变化(ViewTreeObserver.OnGlobalLayoutListener)
使用ViewTreeObserver.OnGlobalLayoutListener可以监听视图树的全局布局事件,当视图树的布局发生变化时会触发回调 。通过这种方式可以在视图布局完成后获取其宽高 。示例代码如下:
View view = findViewById(R.id.my_view);
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除监听,避免多次调用
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
// 处理宽高数据
}
});
这种方法的优点是简单直接,能确保在布局完成后获取宽高 。缺点是只要视图树的布局发生变化,回调就会被触发 。如果布局频繁变化,可能会导致不必要的性能开销 。而且它是在布局变化时触发,不一定是首次布局完成时,所以在某些场景下可能不太适用 。
2. 手动测量(view.measure ())
通过手动调用view.measure(int widthMeasureSpec, int heightMeasureSpec)方法来得到 View 的宽高 。需要根据 View 的LayoutParams情况,使用MeasureSpec.makeMeasureSpec(int size, int mode)拼接出measure()方法的参数 。例如,当 View 的宽高是精确值(如 100dp)时,可以这样测量:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
当 View 的宽高是wrap_content时:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
这种方法的优点是可以在需要的时候主动测量 View 的宽高 。缺点是使用起来相对复杂,需要根据不同的布局参数情况来构造MeasureSpec 。而且只适用于一次完成测量过程的 View,对于一些复杂的布局,如RelativeLayout、TextView以及使用weight属性的LinearLayout等,可能需要多次调用measure()方法才能完成测量,这种情况下手动测量就不太适用了 。