背景
有时我们会有基于这样的需求,当Activity创建时,调用View.getWidth、View.getHeight()、View.getMeasuredWidth() 、View.getgetMeasuredHeight()来获得某个view的宽度或高度,然后进行相应的操作,但是我们在onCreate()、onStart()、onResume()中获取View的大小,获取到的值都是0,这是应为当前activity所代表的界面还没显示出来没有添加到WindowPhone的DecorView上或要获取的view没有被添加到DecorView上或者该View的visibility属性为gone 或者该view的width或height真的为0 ,所以只有上述条件都不成立时才能得到非0的width和height。因为View的measure过程和Activity的生命周期不是同步进行的,不能保证在Activity的onCreate()、onStart()、onResume()方法执行时View已经测量完毕,所以不能获取到正确的宽高,也就是onCreate()、onStart()、onResume()等方法执行完了,我们定义的控件才会被测量,所以我们在onCreate()、onStart()、onResume()里面通过 view.getHeight() 获取控件的高度肯定是0,因为它还没有被测量,View的绘制工程还未完成,和在onCreate中弹出Dialog或者PopupWindow会报一个Activity not running原理类似。
这需要我们去了解一下View的绘制流程,View 需要onMeasure、onLayout、onDraw 三个过程才会真正画出来,执行onMeasure后可以得到 mMeasuredWidth、mMeasuredHeight, 执行onLayout 后可以得到 mLeft、mTop、mRight、mBottom 四个值,getWidth和getHeight就是通过这四个值计算的,如果我们在onCreate() 或 onResume() 中获取View 的宽高值,由于此时View还没measure\layout\draw,所以此时获取的就是0。在measure、layout、draw的每个过程都有方法获取View 的宽高值,下面一一介绍。
获取宽高方式
(1)通过onWindowFocusChanged方法
当整个DecorView已经绘制完成,Activty已经要显示出来的时候,就会回调Activity#onWindowFocusChanged。Activity的窗口得到焦点时,View已经初始化完成,此时获取到的View的宽高是准确的,此方法会被多次调用,当Activity的窗口得到/失去焦点时均会被调用一次,同样的Dialog和PopupWindow也可以在这里弹出,需要注意的是这个方法会调用多次,当hasFocus为true时,才可进行相应的操作
public class GetHeightSampleActivity extends AppCompatActivity {
TextView textView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_get_height);
textView = findViewById(R.id.tv);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
Log.w("tv_width", "" + textView.getWidth());
Log.w("tv_height", "" + textView.getHeight());
}
}
}
(2)通过LayoutParams获取
对于在XML文件里设置了具体宽高的View可以通过view.getLayoutParams().height/width获取到宽高。
优点是能及时获取到,且操作简单;缺点是不够通用,没有设置具体宽高的获取到的值就是0了。
(3)调用View的measure方法
我们可以直接调用 View.measure 来获取 mMeasuredWidth与mMeasuredHeight,但是这个测量宽高可能与实际宽高不一致。
getMeasuredWidth() 与 getMeasuredHeight() 获取的是mMeasuredWidth与mMeasuredHeight,这是在 measure 过程计算出来的测量宽高,而 getWidth() 与 getHeight() 是在 layout 过程之后计算出来的宽高。我们知道 View 的宽高是由 View 本身和 parent 容器共同决定的。mMeasuredWidth与mMeasuredHeight是View 本身的size,但是如果 parent 容器 没有足够大,View就不得不降低自己的尺寸。比如,View 通过自身 measure() 方法向 parent 请求 100x100 的宽高,那么这个宽高就是 measuredWidth 和 measuredHeight 值。但是,在 parent 的 onLayout() 阶段,通过 childview.layout() 方法只分配给 childview 50x50 的宽高。那么,这个 50x50 宽高就是 childview 实际绘制并显示到屏幕的宽高,也就是 width 和 height 值。
public class MainActivity extends AppCompatActivity {
private View view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
view = findViewById(R.id.view);
view.measure(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
int width = view.getMeasuredWidth();
Log.i("TAG", "width=" +width);
}
}
我们在 onCreate() 中要用到控件的宽高的时候可以主动去为它测量,也就是直接调用一个 view 或者 viewgroup 的 measure() 去测量。
测量之后该 view 的 getMeasuredHeight() 就会返回刚才测量所得的高,getMeasuredWidth() 返回测量所得宽。
本来在布局加载的过程中,view的 measure方法一定会被系统调用(在onResume() 中已经调用了 measure方法),但这发生在我们所不知道的某个时间点,为了在这之前提前得到测量结果,我们主动调用 measure方法,但是这样做的好处是可以立即获得宽和高,坏处是多了一次测量过程。
对于宽高设为具体数值或wrap_content的控件,我们都可以手动构造MeasureSpec,而match_parent的情况是做不到的,match_parent无法measure出具体的宽/高,原因很简单,根据View的measure过程,构造此种MeasureSpec需要父view的size,即父容器的剩余空间,
所以我们需要在measure时传递正确的父View的Size,而此时我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
对于设置了具体数值宽高的(比如都是100px),我们可以这样构造MeasureSpec:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
对于设置成wrap_content的:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOTST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
注意到(1<<30)-1,通过分析MeasureSpec可知,View的尺寸使用30位二进制表示,也就是说最大是30个1(即2^30-1),也就是(1<<30)-1,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。
关于View的measure,网上有两个错误的用法。
为什么说是错误的,首先起违背了系统的内部实现规范,因为无法通过错误的MeasureSpec去得出合法的SpecMode,从而导致measure过程出错,其次不能保证一定能measure出正确的结果。
第一种错误用法:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
mView.measure(widthMeasureSpec, heightMeasureSpec);
第二种错误用法:
mView.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
但是这种测试可以得到宽高。。。待研究。
优点也是可以立即获取到宽高;缺点是无法解决match_parent的情况,该方法测量的宽度和高度可能与视图绘制完成后的真实的宽度和高度不一致。
(4)OnGlobalLayoutListener获取
另一种方法是利用 ViewTreeObserver 的 OnGlobalLayoutListener 来测量
在布局发生改变或者某个视图的可视状态发生改变时调用该事件,会被多次调用,因此需要在获取到视图的宽度和高度后执行 remove 方法移除该监听事件。
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
final int w = view.getMeasuredWidth();
final int h = view.getMeasuredHeight();
}
});
(5)OnPreDrawListener获取
在视图将要绘制时调用该监听事件,会被调用多次,因此获取到视图的宽度和高度后要移除该监听事件。这同样是 ViewTreeObserver 的接口。
OnPreDrawListener是在draw之前的回调,此时已经 layout 过,可以获取到 View 的宽高值。OnPreDrawListener还可以控制绘制流程,返回false的时候就取消当前绘制流程,View会再schedule下一次绘制:
view.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
int w = view.getWidth();
int h = view.getHeight();
return true;
}
}
);
(6)OnLayoutChangeListener获取
在视图的 layout 改变时调用该事件,会被多次调用,因此需要在获取到视图的宽度和高度后执行 remove 方法移除该监听事件。
view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
view.removeOnLayoutChangeListener(this);
int w = view.getWidth();
int h = view.getHeight();
}
});
从中可以发现在view本身的layout时才会回调OnLayoutChangeListener。而对于OnGlobalLayoutListener, 不管view本身或其父view的layout时都会回调。
(7)重写View的onSizeChanged()
在视图的大小发生改变时调用该方法,会被多次调用,因此获取到宽度和高度后需要考虑禁用掉代码。
该实现方法需要继承 View,且多次被调用,不建议使用。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.i("TAG", "width = " + getWidth() + "height = " + getHeight());
}
(8)重写View的onLayout()
该方法会被多次调用,获取到宽度和高度后需要考虑禁用掉代码。
对于自定义的View,可以重写 onLayout() 并在此函数中获取宽高,且多次被调用,不建议使用。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.i("TAG", "width = " + getWidth() + "height = " + getHeight());
}
补充一点,getMeasuredWidth() 与 getWidth() 或者 getMeasuredHeight() 与 getHeight() 的区别。measuredWidth 与 width 分别对应于视图绘制 的 measure 与 layout 阶段。View 的宽高是由 View 本身和 parent 容器共同决定的,要知道有这个 MeasureSpec 类的存在。
比如,View 通过自身 measure() 方法向 parent 请求 100x100 的宽高,那么这个宽高就是 measuredWidth 和 measuredHeight 值。但是,在 parent 的 onLayout() 阶段,通过 childview.layout() 方法只分配给 childview 50x50 的宽高。那么,这个 50x50 宽高就是 childview 实际绘制并显示到屏幕的宽高,也就是 width 和 height 值。
(9)使用View.post()方法
这里的view可以是你需要获取宽高的View。要注意的是view要执行此方法必须保证它已经attached到了window上,因此在此之前是不能调用这个方法的。
而通过view.post()在主线程的消息队列尾部插入了一个消息,也就是说执行获取宽高的操作被延后了,并且能够保证Measure操作在此之前,所以就能够在这里获取到正确的宽高了。
Runnable 对象中的方法会在 View 的 measure()、layout() 等事件完成后触发。
UI 事件队列会按顺序处理事件,在 setContentView() 被调用后,事件队列中会包含一个要求重新 layout 的 message,所以任何 post 到队列中的 Runnable 对象都会在 Layout 发生变化后执行。
该方法只会执行一次,且逻辑简单,建议使用。
此方法优点是保证获取到的宽高是准确的;
缺点是不能及时获取到,实际上还是把操作延后了,需要在Runnable里再执行相应回调。
view.post(new Runnable() {
@Override
public void run() {
Log.i("TAG", "width = " + view.getWidth() +
"height = " + view.getHeight());
}
});
(10)ViewCompat.isLaidOut(view)
if (ViewCompat.isLaidOut(view)) {
int width = view.getWidth();
}
严格来讲,这不能作为一个获取宽高的方式之一。充其量只能是一个判断条件。只有当 View 至少经历过一次 layout 时,isLaidOut() 方法才能返回 true,继而才能获取到 View 的真实宽高。所以,当我们的代码中有多次调用获取宽高时,才有可能使用这个方法判断处理。
本文是学习记录,仅供参考