天天看點

如何在onCreate中測量View的實際寬高

通常在實際開發中,為了适配,我們會把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中測量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

繼續閱讀