Activity / Fragment 的生命周期和 View 绘制过程的各个阶段是相互独立的,所以我们在 onCreate() 里面使用 getWidth()/getMeasuredWidth() 往往得到 0。
得到 View 宽高的正确方式有如下几种:
- ViewTreeObserver.OnPreDrawListener;
- ViewTreeObserver.OnGlobalLayoutChangeListener;
- View.measure(int widthMeasureSpec, int heightMeasureSpec, );
- 自定义控件;
前两种方法都是采用回调的方式,优点是简单、百分百正确,缺点是某些场景下局限较多、作用有限。
第三种方法的原理是模拟 View 的 measure 过程,执行完 measure 过程后再调用 View.getMeasuredWidth() / View.getMeasuredHeight() 得到的即为所求,但是局限如下:
- 只适用于一次完成 measure 过程的 View,而无法完全适用于一次 measure 过程需要多次调用 measure() 方法的 View (如 RelativeLayout、TextView 以及使用 weight 属性的 LinearLayout 等);
- 需要使用 MeasureSpec.makeMeasureSpec(int size, int mode)拼接出 measure() 方法的参数;
对于更复杂的场景,建议使用第四种方法——自定义控件(甚至可以直接继承 ViewGroup),在 onMeasure() 方法里面获得子 View 的宽高。
此外,坊间流传的另一种方法:
view.post(new Runnable(){
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
这种方法确实有效,但是却很侥幸。
之所以有效,是因为主线程开始执行 ViewRootImpl.scheduleTraversals()
方法的时候,会在主线程的消息队列的头部插入一个 Message,该 Message 的 target 为 null,asynchronous 标志位为 false 的同步消息,称为同步屏障(以下代码出自 API 23):
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
当检测到该屏障时,系统就会跳过所有的同步消息,找到第一个异步消息,也就是执行 ViewRootImpl.scheduleTraversals()
消息,先执行该消息,以保证正常绘制帧、避免出现卡顿。
当 traversal 过程完成了,measure + layout + draw 过程都完成了,实际的宽高也都计算完毕。这个时候再执行 post 出去的消息,就能得到正确的宽高值。
具体原理见 【Android源码解析】View.post()到底干了啥。
这是一个很鸡贼的方法,不是系统预设的方式,所以不建议使用该方法。
参考文章