Android ViewTreeObserver使用總結


官方文檔的描述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的變化:

 
image.png

二.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; } 

效果

 
image.png

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


 
image.png

關於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 = (LayoutParams) checkChild.getLayoutParams(); final Behavior b = checkLp.getBehavior(); if (b != null && b.layoutDependsOn(this, checkChild, child)) { if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) { // If this is not from a nested scroll and we have already been changed // from a nested scroll, skip the dispatch and reset the flag checkLp.resetChangedAfterNestedScroll(); continue; } //調用onDependentViewChanged final boolean handled = b.onDependentViewChanged(this, checkChild, child); ... } } } } 

三.OnGlobalLayoutListener

Interface definition for a callback to be invoked when the global layout state or the visibility of views within the view tree changes. 

當在一個視圖樹中全局布局發生改變或者視圖樹中的某個視圖的可視狀態發生改變時,所要調用的回調函數的接口類

代碼

1.在點擊時改變EditText的可視性。

    @Override public void onClick(View v) { if(et_1.isShown()){ et_1.setVisibility(View.GONE); }else{ et_1.setVisibility(View.VISIBLE); } } 

2.在onGlobalLayout顯示EditText的可見性

  @Override public void onGlobalLayout() { if(et_1.isShown()){ tv_show.setText("EditText1 顯示"); }else{ tv_show.setText("EditText1 隱藏"); } } 

效果

 
這里寫圖片描述

注意:在測試的時候發現使用

et_1.setVisibility(View.INVISIBLE); 

時並不會觸發OnGlobalLayoutListener而只能使用

  et_1.setVisibility(View.GONE); 

補充

可以使用OnGlobalLayoutListener獲取控件寬高。

private int mHeaderViewHeight; private View mHeaderView; ..... mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener( new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mHeaderViewHeight = mHeaderView.getHeight(); mHeaderView.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } });


作者:遙遙的遠方
鏈接:https://www.jianshu.com/p/4f68d62c809b
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM