Android ViewTreeObserver使用總結及獲得View高度的幾種方法

_小馬快跑_發表於2017-12-15

ViewTreeObserver 註冊一個觀察者來監聽檢視樹,當檢視樹的佈局、檢視樹的焦點、檢視樹將要繪製、檢視樹滾動等發生改變時,ViewTreeObserver都會收到通知,ViewTreeObserver不能被例項化,可以呼叫View.getViewTreeObserver()來獲得。

ViewTreeObserver繼承關係:

public final class ViewTreeObserverextendsObject

java.lang.Object
↳android.view.ViewTreeObserver
複製程式碼

ViewTreeObserver直接繼承自Object.

ViewTreeObserver提供了View的多種監聽,每一種監聽都有一個內部類介面與之對應,內部類介面全部儲存在CopyOnWriteArrayList中,通過ViewTreeObserver.addXXXListener()來新增這些監聽,原始碼如下:

public final class ViewTreeObserver {
    // Recursive listeners use CopyOnWriteArrayList
    private CopyOnWriteArrayList<OnWindowFocusChangeListener> mOnWindowFocusListeners;
    private CopyOnWriteArrayList<OnWindowAttachListener> mOnWindowAttachListeners;
    private CopyOnWriteArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
    private CopyOnWriteArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
    private CopyOnWriteArrayList<OnEnterAnimationCompleteListener> mOnEnterAnimationCompleteListeners;

    // Non-recursive listeners use CopyOnWriteArray
    // Any listener invoked from ViewRootImpl.performTraversals() should not be recursive
    private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
    private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners;
    private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;
    private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
    private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners;

    // These listeners cannot be mutated during dispatch
    private ArrayList<OnDrawListener> mOnDrawListeners;
}
複製程式碼

以OnGlobalLayoutListener為例,首先是定義介面:

 public interface OnGlobalLayoutListener {
        /**
         * Callback method to be invoked when the global layout state or the visibility of views
         * within the view tree changes
         */
        public void onGlobalLayout();
    }
複製程式碼

將OnGlobalLayoutListener 新增到CopyOnWriteArray陣列中:

  /**
     * Register a callback to be invoked when the global layout state or the visibility of views
     * within the view tree changes
     *
     * @param listener The callback to add
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     */
    public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
        checkIsAlive();

        if (mOnGlobalLayoutListeners == null) {
            mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
        }

        mOnGlobalLayoutListeners.add(listener);
    }
複製程式碼

移除OnGlobalLayoutListener,當檢視樹佈局發生變化時不會再收到通知了:

/**
     * Remove a previously installed global layout callback
     *
     * @param victim The callback to remove
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     * 
     * @deprecated Use #removeOnGlobalLayoutListener instead
     *
     * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
     */
    @Deprecated
    public void removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) {
        removeOnGlobalLayoutListener(victim);
    }

    /**
     * Remove a previously installed global layout callback
     *
     * @param victim The callback to remove
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     * 
     * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
     */
    public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
        checkIsAlive();
        if (mOnGlobalLayoutListeners == null) {
            return;
        }
        mOnGlobalLayoutListeners.remove(victim);
    }
複製程式碼

其他常用方法:

**dispatchOnGlobalLayout():**檢視樹發生改變時通知觀察者,如果想在View Layout 或 View hierarchy 還未依附到Window時,或者在View處於GONE狀態時強制佈局,這個方法也可以手動呼叫。

   /**
     * Notifies registered listeners that a global layout happened. This can be called
     * manually if you are forcing a layout on a View or a hierarchy of Views that are
     * not attached to a Window or in the GONE state.
     */
    public final void dispatchOnGlobalLayout() {
        // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
        // perform the dispatching. The iterator is a safe guard against listeners that
        // could mutate the list by calling the various add/remove methods. This prevents
        // the array from being modified while we iterate it.
        final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    access.get(i).onGlobalLayout();
                }
            } finally {
                listeners.end();
            }
        }
    }
複製程式碼

**dispatchOnPreDraw():**通知觀察者繪製即將開始,如果其中的某個觀察者返回 true,那麼繪製將會取消,並且重新安排繪製,如果想在View Layout 或 View hierarchy 還未依附到Window時,或者在View處於GONE狀態時強制繪製,可以手動呼叫這個方法。

 /**
     * Notifies registered listeners that the drawing pass is about to start. If a
     * listener returns true, then the drawing pass is canceled and rescheduled. This can
     * be called manually if you are forcing the drawing on a View or a hierarchy of Views
     * that are not attached to a Window or in the GONE state.
     *
     * @return True if the current draw should be canceled and resceduled, false otherwise.
     */
    @SuppressWarnings("unchecked")
    public final boolean dispatchOnPreDraw() {
        boolean cancelDraw = false;
        final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    cancelDraw |= !(access.get(i).onPreDraw());
                }
            } finally {
                listeners.end();
            }
        }
        return cancelDraw;
    }
複製程式碼

ViewTreeObserver常用內部類:

內部類介面 備註
ViewTreeObserver.OnPreDrawListener 當檢視樹將要被繪製時,會呼叫的介面
ViewTreeObserver.OnGlobalLayoutListener 當檢視樹的佈局發生改變或者View在檢視樹的可見狀態發生改變時會呼叫的介面
ViewTreeObserver.OnGlobalFocusChangeListener 當一個檢視樹的焦點狀態改變時,會呼叫的介面
ViewTreeObserver.OnScrollChangedListener 當檢視樹的一些元件發生滾動時會呼叫的介面
ViewTreeObserver.OnTouchModeChangeListener 當檢視樹的觸控模式發生改變時,會呼叫的介面

獲得View高度的幾種方式:

我們應該都遇到過在onCreate()方法裡面呼叫view.getWidth()和view.getHeight()獲取到的view的寬高都是0的情況,這是因為在onCreate()裡還沒有執行測量,需要在onResume()之後才能得到正確的高度,那麼可不可以在onCreate()裡就得到寬高呢?答:可以!常用的有下面幾種方式:

1、通過設定View的MeasureSpec.UNSPECIFIED來測量:

int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(w, h);
//獲得寬高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();
複製程式碼

設定我們的SpecMode為UNSPECIFIED,然後去呼叫onMeasure測量寬高,就可以得到寬高。

2、通過ViewTreeObserver .addOnGlobalLayoutListener來獲得寬高,當獲得正確的寬高後,請移除這個觀察者,否則回撥會多次執行:

//獲得ViewTreeObserver 
ViewTreeObserver observer=view.getViewTreeObserver();
//註冊觀察者,監聽變化
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
     @Override
     public void onGlobalLayout() {
            //判斷ViewTreeObserver 是否alive,如果存活的話移除這個觀察者
           if(observer.isAlive()){
             observer.removeGlobalOnLayoutListener(this);
             //獲得寬高
             int viewWidth=view.getMeasuredWidth();
             int viewHeight=view.getMeasuredHeight();
           }
        }
   });
複製程式碼

3、通過ViewTreeObserver .addOnPreDrawListener來獲得寬高,在執行onDraw之前已經執行了onLayout()和onMeasure(),可以得到寬高了,當獲得正確的寬高後,請移除這個觀察者,否則回撥會多次執行

//獲得ViewTreeObserver 
ViewTreeObserver observer=view.getViewTreeObserver();
//註冊觀察者,監聽變化
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
       @Override
       public boolean onPreDraw() {
          if(observer.isAlive()){
            observer.removeOnDrawListener(this);
             }
          //獲得寬高
           int viewWidth=view.getMeasuredWidth();
           int viewHeight=view.getMeasuredHeight();
           return true;
     }
   });
複製程式碼

相關文章