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() 中,控件尚未繪制完成,那麼將代碼寫在更晚執行的一些生命周期函數中,問題是不是能得到解決呢?之後筆者分別在 Actvity 中的一些常用的 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(mRunnabel) 並不能得到正確的結果。
很多教程中,通過延遲一個毫秒數,即 new Handler().postDelayed(mRunnabel, 500),來調用執行實際測量工作的 Runnable。這樣雖然能得到正確的結果,但顯然不是一個好的解決方案:太小的毫秒數不能保證控件完成繪制,太大的毫秒數,會嚴重破壞用戶的操作體驗。