onCreate() 中 View 尚未绘制完成
很多时候,我们需要在某个界面刚刚加载的时候,执行一些对 View
进行操作的代码,通常我们把这些代码放在 Activity
的 onCreate()
方法或 Fragment
的 onCreateView()
方法中。但因为在这些 Hook 方法被调用时,View
并没有绘制完成,很多操作不能生效。
比如,在 onCreate()
中调用某个按钮的 myButton.getHeight()
,得到的结果永远是0。不想花时间慢慢看的同学,可以直接去看最后结论中的解决方案。
Button myButton = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
myButton = (Button) findViewById(R.id.button1);
// 尝试在 onCreate() 中获取控件尺寸
int h = myButton.getHeight();
Log.i(TAG, "onCreate(): Height=" + h); // 得到的结果是 Height=0
// ...
}
类似的情况还有很多,笔者之前自己写了一个下拉刷新的 ListView
,希望在 Fragment
完成绘制后,立刻执行一次刷新,并将代码写在了 onCreateView()
中。结果是,列表确实进行了刷新操作,但是没有下拉刷新的动画。
更晚调用的生命周期函数
笔者开始以为,既然 onCreate()
中,控件尚未绘制完成,那么将代码写在更晚执行的一些生命周期函数中,问题是不是能得到解决呢?之后笔者分别在 Activity
中的一些常用的 Hook 函数中,尝试获取控件的尺寸,得到如下结果(按被执行的顺序排列):
onCreate(): Height=0
onStart(): Height=0
onPostCreate(): Height=0
onResume(): Height=0
onPostResume(): Height=0
onAttachedToWindow(): Height=0
onWindowsFocusChanged(): Height=1845
可以看到,直到 onWinodwsFocusChanged()
函数被调用,我们才能得到正确的控件尺寸。其他 Hook 函数,包括在官方文档中,描述为在 Activity
完全启动后才调用的 onPostCreate()
和 onPostResume()
函数,均不能得到正确的结果。
遗憾的是,虽然在 onWinodwsFocusChanged()
函数中,可以得到正确的控件尺寸。但这只在 Activity
中奏效,而在 Fragment
中,该方法并不能生效。
更多的解决方案
1. 使用 ViewTreeObserver 提供的 Hook 方法。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
myButton = (Button) findViewById(R.id.button1);
// 向 ViewTreeObserver 注册方法,以获取控件尺寸
ViewTreeObserver vto = myButton.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
int h = myButton.getHeight();
Log.i(TAG, "Height=" + h); // 得到正确结果
// 成功调用一次后,移除 Hook 方法,防止被反复调用
// removeGlobalOnLayoutListener() 方法在 API 16 后不再使用
// 使用新方法 removeOnGlobalLayoutListener() 代替
myButton.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
// ...
}
该方法在 onGlobalLayout()
方法将在控件完成绘制后调用,因而可以得到正确地结果。该方法在 Fragment
中,也可以使用。
2. 使用 View 的 post() 方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
myButton = (Button) findViewById(R.id.button1);
// 使用myButton 的 post() 方法
myButton.post(new Runnable() {
@Override
public void run() {
int h = myButton.getHeight();
Log.i(TAG, "Height=" + h); // 得到正确结果
}
});
// ...
}
该方法同样在 Fragment
中适用,是目前笔者发现的最佳解决方案。
注意:通过调用测量对象的 post()
方法注册的 Runnable
总在对象完全绘制后才调用,所以可以得到正确的结果。但直接在 onCreate()
中,使用 new Handler().post(mRunnable)
并不能得到正确的结果。
很多教程中,通过延迟一个毫秒数,即 new Handler().postDelayed(mRunnable, 500)
,来调用执行实际测量工作的 Runnable
。这样虽然能得到正确的结果,但显然不是一个好的解决方案:太小的毫秒数不能保证控件完成绘制,太大的毫秒数,会严重破坏用户的操作体验。
结论
如果需要在某个 View
完成绘制后,立刻执行一段代码(如需要获取控件尺寸),最优雅的做法是:在 onCreate()
等生命周期函数中,调用该 View
的 post()
方法。
(参见上面的代码)
使用 ViewTreeObserver
注册 OnGlobalLayoutListener
,或使用 Activity
的 onWinodwsFocusChanged()
方法,也可以达到相同目的。