通常在實際開發中,為了适配,我們會把View設定為match_parent或者wrap_content、又或者是設定weight權重來配置設定寬高,而不是使用具體值。那麼就出現一個問題了,如果動态測量View的實際寬高。
其實這個問題網上有很多解決方法,無奈給出的方法衆多,卻沒有解析,實際可能根本不管用。是以詳細記錄下這個問題。
測量寬高的API
我們以高度為例子:
android提供了兩個API來動态擷取View的高:
- view.getMeasuredHeight()
- view.getHeight()
那麼這兩個API的差別是什麼呢?
簡單來說就是:
- view.getMeasuredHeight() 是由view中的測量方法指派的,這個值包含了隐藏的高度(比如一個view部分超出螢幕,他也會計算出來)。
- view.getHeight() 由view的底部位置減去頂部位置,即實際顯示的View的高度,不包含隐藏了的高度。
如果是在View的onMeasure方法後執行上面兩個方法,會發現可以得到正确的高度,但是如果是在onCreate等方法中執行就會發現傳回的值不正确或者為0,這是為什麼呢?
Activity和View的混合生命周期
我畫了一個圖來表明兩者的生命周期先後問題:
可以看到的是onCreate()和onResume()是在onMeasure方法之前。
說明此時還沒有測量出實際的寬高,還沒有進行繪制,是以調用上述的兩個API會出現值為0或者數值錯誤的情況。
經過驗證的解決方法
下面的方法都是進過驗證的,但是有使用的條件限制,務必注意。
1.主動測量
代碼:
view.measure(, );
view.getMeasuredWidth();
view.getMeasuredHeight();
說明:
onMeasure傳入的兩個參數是由父控件的大小,
也可以使用
View.MeasureSpec.makeMeasureSpec(0,mode);
設定值
其中mode可以選擇
- MeasureSpec.UNSPECIFIED 未指定尺寸,比如listview中尺寸由父控件決定
- MeasureSpec.EXACTLY 适合match_parent或者具體值
- MeasureSpec.AT_MOST 适合wrap_content不确定值
注意:
這種方法不一定能測出正确的值,因為onMesure會多次調用(由于onMesure自上而下,父控件如果對于子控件的寬高不滿意,即如子控件沒有限制寬高,父控件會重新調用onMesure重新測量),是以測量結果不一定正确。
2.使用OnPreDrawListener
代碼:
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
return true;
}
});
說明:
這個是異步解決方法,在onDraw之前執行,onDraw方法執行在onMeasure之後,是以這個監聽器可以解決這個問題。
注意:
onPreDraw方法隻有return true才會生效,由于onDraw會在view繪制時調用,是以可能會調用多次,務必調用removeOnPreDrawListener方法,防止執行效率問題。onDraw會多次執行,每次view出現都會調用,但onLayout不會,測量後會被記錄下來。
3.使用OnGlobalLayoutListener
代碼:
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int height = img.getWidth();
int width = img.getHeight();
}
});
說明:
這個也是異步解決方法,在全局onLayout觸發時執行。
注意:
這個方法有一定不确定性,而且onLayout執行第一次的數值不一定正确。
4.使用OnLayoutChangeListener
代碼:
view.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
}
});
說明:
這個方法OnLayout時調用,而且隻有改變時才會觸發。
注意:
這個方法要求API level 11
5.低版本API使用OnLayoutChangeListener相容方法
低版本的可以通過自定義控件,這裡以ImageView為例,個人設計的自定義ImageView控件:
/**
* 封裝OnLayoutChangeListener的ImageView,為相容低版本
*/
public class LayoutImageView extends ImageView {
public LayoutImageView(Context context) {
super(context);
}
public LayoutImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 相容低版本的監聽器集合
*/
private ArrayList<SupportOnLayoutChangeListener> onLayoutChangeListeners;
/**
* 添加布局改變監聽器
* @param listener 監聽器
*/
public void addOnLayoutChangeListener(SupportOnLayoutChangeListener listener) {
if (onLayoutChangeListeners == null) {
onLayoutChangeListeners = new ArrayList<SupportOnLayoutChangeListener>();
}
if (!onLayoutChangeListeners.contains(listener)) {// 不重複添加
onLayoutChangeListeners.add(listener);
}
}
/**
* 移除監聽器
* @param listener 監聽器
*/
public void removeOnLayoutChangeListener(
SupportOnLayoutChangeListener listener) {
if (onLayoutChangeListeners == null || listener == null) {
return;
}
onLayoutChangeListeners.remove(listener);;// 先不删除
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (onLayoutChangeListeners != null) {
@SuppressWarnings("unchecked")
ArrayList<SupportOnLayoutChangeListener> listenersCopy = (ArrayList<SupportOnLayoutChangeListener>) onLayoutChangeListeners
.clone();//使用克隆集合,防止多線程操作同一集合問題
int numListeners = listenersCopy.size();
for (int i = ; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this);
}
}
}
/**
* 相容低版本的布局改變監聽器
*/
public interface SupportOnLayoutChangeListener {
/**
* 布局改變
* @param thisView 改變的View
*/
public void onLayoutChange(LayoutImageView thisView);
}
}
上述代碼是參考了谷歌View上的addOnLayoutChangeListener等方法的寫法,其中clone集合這種設計方法非常值得借鑒,可以解決多線程通路同一個集合,又不用考慮同步問題。
這樣就可以在低版本中使用相容的addOnLayoutChangeListener,當然下面的imageView是自定義的LayoutImageView類型:
imageView.addOnLayoutChangeListener(
new SupportOnLayoutChangeListener() {
@Override
public void onLayoutChange(LayoutImageView thisView) {
thisView.removeOnLayoutChangeListener(this);
}
});
聲明
原創文章,歡迎轉載,請保留出處。
有任何錯誤、疑問或者建議,歡迎指出。
我的郵箱:[email protected]
參考文章
http://blog.csdn.net/crazy1235/article/details/41806079