官方文檔的描述ViewTreeObserver是用來監聽一些全局變化的。
在 ViewTreeObserver 中,包含了以下幾個接口:
-
interface ViewTreeObserver.OnGlobalFocusChangeListener
-
interface ViewTreeObserver.OnGlobalLayoutListener
-
interface ViewTreeObserver.OnPreDrawListener
-
interface ViewTreeObserver.OnScrollChangedListener
-
interface ViewTreeObserver.OnTouchModeChangeListener
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. */
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; } });
案例分析
xml布局如下
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:gravity="center" android:layout_height="match_parent" android:orientation="vertical" android:id="@+id/layout" tools:context="trs.com.viewtreeobserverdemo.MainActivity"> <TextView android:id="@+id/tv_show" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:hint="et1" android:tag="et1" android:id="@+id/et_1" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:tag="et2" android:hint="et2" android:layout_marginTop="10dp" android:id="@+id/et_2" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:text="test" android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
讓MainActivity實現相應接口
public class MainActivity extends AppCompatActivity implements ViewTreeObserver.OnGlobalLayoutListener, ViewTreeObserver.OnPreDrawListener, ViewTreeObserver.OnGlobalFocusChangeListener, ViewTreeObserver.OnTouchModeChangeListener, View.OnClickListener
在onCreat中添加監聽
EditText et_1,et_2; TextView tv_show; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewTreeObserver vto = findViewById(R.id.layout).getViewTreeObserver(); et_1= (EditText) findViewById(R.id.et_1); et_2= (EditText) findViewById(R.id.et_2); vto.addOnGlobalLayoutListener(this); vto.addOnPreDrawListener(this); vto.addOnGlobalFocusChangeListener(this); vto.addOnTouchModeChangeListener(this); findViewById(R.id.btn).setOnClickListener(this); tv_show= (TextView) findViewById(R.id.tv_show); }
一.OnGlobalFocusChangeListener
首先測試ViewTreeObserver.OnGlobalFocusChangeListener,實現接口方法
代碼
@Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { if(oldFocus!=null) { tv_show.setText("Focus change from " + oldFocus.getTag() + " to " + newFocus.getTag()); }else{ tv_show.setText( newFocus.getTag()+"get focus"); } }
注意:在第一次進入頁面的時候沒有oldFoucs
效果
這個接口很簡單就是監聽focus的變化:

二.OnPreDrawListener
OnPreDrawListener接口是在繪制界面前調用
代碼
@Override public boolean onPreDraw() { et_1.setHint("set hint on onPreDraw "); //Return true to proceed with the current drawing pass, or false to cancel. //返回 true 繼續繪制,返回false取消。 return true; }
效果

如果返回false的話,效果是這樣的,界面沒有繪制。

關於OnPreDrawListener的使用,有一個例子就是CoordinatorLayout調用Behavior的onDependentViewChanged就是通過注冊OnPreDrawListener接口,在繪制的時候檢查界面是否發生變化,如果變化就調用Behavior的onDependentViewChanged。
源碼
@Override public void onAttachedToWindow() { super.onAttachedToWindow(); resetTouchBehaviors(); if (mNeedsPreDrawListener) { if (mOnPreDrawListener == null) { mOnPreDrawListener = new OnPreDrawListener(); } //注冊OnPreDrawListener final ViewTreeObserver vto = getViewTreeObserver(); vto.addOnPreDrawListener(mOnPreDrawListener); } if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) { // We're set to fitSystemWindows but we haven't had any insets yet... // We should request a new dispatch of window insets ViewCompat.requestApplyInsets(this); } mIsAttachedToWindow = true; } class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener { @Override //分發OnDependentViewChanged dispatchOnDependentViewChanged(false); return true; } } void dispatchOnDependentViewChanged(final boolean fromNestedScroll) { final int layoutDirection = ViewCompat.getLayoutDirection(this); final int childCount = mDependencySortedChildren.size(); for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); // Check child views before for anchor for (int j = 0; j < i; j++) { final View checkChild = mDependencySortedChildren.get(j); if (lp.mAnchorDirectChild == checkChild) { offsetChildToAnchor(child, layoutDirection); } } //判斷是否發生變化 // Did it change? if not continue final Rect oldRect = mTempRect1; final Rect newRect = mTempRect2; getLastChildRect(child, oldRect); getChildRect(child, true, newRect); if (oldRect.equals(newRect)) { continue; } recordLastChildRect(child, newRect); // Update any behavior-dependent views for the change for (int j = i + 1; j < childCount; j++) { final View checkChild = mDependencySortedChildren.get(j); final LayoutParams checkLp =