Android 开发探秘:View.post()为何能获取View宽高

Android 开发探秘:View.post()为何能获取View宽高

开发中的小困惑

在 Android 开发的奇妙世界里,获取 View 的宽高是一个常见的操作,但也常常让开发者们感到困惑。你是否曾经遇到过这样的情况:在 Activity 的onCreate方法中,信心满满地调用view.getWidth()view.getHeight(),满心期待能得到正确的宽高值,结果却得到了 0?这是不是让你感到十分疑惑,甚至有点抓狂呢🤯

例如,我们在布局文件中定义了一个简单的 TextView:

xml 复制代码
<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方法中尝试获取它的宽高:

java 复制代码
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 的宽高。就像这样:

java 复制代码
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 的onCreateonStartonResume等生命周期方法中尝试获取 View 的宽高 。但往往会得到令人失望的 0 值。这是为什么呢🧐

这要从 View 的绘制流程说起。View 的绘制流程主要分为三个阶段:测量(measure)、布局(layout)和绘制(draw)。在测量阶段,系统会计算 View 的宽高,确定measuredWidthmeasuredHeight;布局阶段则确定 View 在父容器中的位置;最后绘制阶段将 View 绘制到屏幕上 。

而在onCreateonStart等方法执行时,View 还处于初始化阶段,远远没有完成测量和布局过程。就好比你正在建造一座房子,房子还只是画了设计图,连地基都还没打,你就问房子有多高多大,这显然是不合理的,因为它还没有成型呢。所以此时调用view.getWidth()view.getHeight()方法获取宽高,得到的自然是 0 。只有当 View 完成了测量和布局,这些方法才能获取到正确的宽高值 。这也就解释了为什么在常规的生命周期方法中直接获取宽高会失败。

View.post () 神奇揭秘

1. 源码解读

要解开view.post()能获取 View 宽高的谜题,我们得深入到它的源码中去。View.post()方法的源码如下:

java 复制代码
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对象:

java 复制代码
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

HandlerActionQueue类的post方法如下:

java 复制代码
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方法中:

java 复制代码
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    // 其他代码...
}

View执行dispatchAttachedToWindow方法时,会给mAttachInfo赋值,并且会执行之前缓存的任务 。而dispatchAttachedToWindow方法是在 View 绘制、测量的时候被调用的 。具体来说,是在ViewRootImplperformTraversals方法中,会遍历DecorView中的子 View 并执行子 View 的dispatchAttachedToWindow方法 。这就像是一场精心安排的演出,在合适的时机,那些被缓存的任务就会被拿出来执行。

3. 执行时机探究

结合前面提到的 View 绘制流程,我们可以更清楚地理解View.post()获取宽高的原理 。在 Activity 的onCreateonStart等方法执行时,View 还没有开始绘制,attachInfo为空,View.post()的任务被缓存 。当执行到onResume之后,ViewRootImplperformTraversals方法被调用,开始进行 View 的测量(measure)和布局(layout) 。在这个过程中,View会执行dispatchAttachedToWindow方法,attachInfo被赋值,之前缓存的View.post()任务被执行 。此时,View 已经完成了测量和布局,所以在View.post()Runnable中就可以获取到正确的宽高值了 。而且,这些任务的执行是在首帧绘制之前,确保了我们能及时获取到宽高用于后续的操作 。

对比其他获取宽高方法

除了view.post()方法,还有一些其他方法可以获取 View 的宽高 ,它们各有特点和适用场景 。

1. 监听布局变化(ViewTreeObserver.OnGlobalLayoutListener)

使用ViewTreeObserver.OnGlobalLayoutListener可以监听视图树的全局布局事件,当视图树的布局发生变化时会触发回调 。通过这种方式可以在视图布局完成后获取其宽高 。示例代码如下:

java 复制代码
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)时,可以这样测量:

java 复制代码
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时:

java 复制代码
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,对于一些复杂的布局,如RelativeLayoutTextView以及使用weight属性的LinearLayout等,可能需要多次调用measure()方法才能完成测量,这种情况下手动测量就不太适用了 。

相关推荐
huabiangaozhi2 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端
umeelove352 小时前
Spring boot整合quartz方法
java·前端·spring boot
爱学习的程序媛2 小时前
【Web前端】WebAssembly详解
前端·web·wasm
不会写DN3 小时前
Js常用的字符串处理
开发语言·前端·javascript
晓13133 小时前
第三章 TypeScript 高级类型
前端·javascript·typescript
一勺菠萝丶3 小时前
芋道项目部署时,前端和门户网站如何通过 Nginx 转发后台接口,而不直接暴露后端地址
运维·前端·nginx
黑白两客3 小时前
Vue 缓存机制
前端·vue.js·缓存
Luna-player3 小时前
Vue 组件,用来实现一个响应式图标网格布局,核心是用 CSS 实现固定宽高比的正方形容器,并在里面放置图片和文字。
前端·css·vue.js
陈随易3 小时前
深度拆解技术架构的三大鸿沟:企业级Claw vs OpenClaw的工程差异
前端·后端·程序员