Android ViewTreeObserver使用總結(jié)

官方文檔的描述ViewTreeObserver是用來監(jiān)聽一些全局變化的样漆。

在 ViewTreeObserver 中怒见,包含了以下幾個接口:

  • interface ViewTreeObserver.OnGlobalFocusChangeListener

  • interface ViewTreeObserver.OnGlobalLayoutListener

  • interface ViewTreeObserver.OnPreDrawListener

  • interface ViewTreeObserver.OnScrollChangedListener

  • interface ViewTreeObserver.OnTouchModeChangeListener

ViewTreeObserver 注冊一個觀察者來監(jiān)聽視圖樹盯拱,當視圖樹的布局事甜、視圖樹的焦點订歪、視圖樹將要繪制穿肄、視圖樹滾動等發(fā)生改變時年局,ViewTreeObserver都會收到通知,ViewTreeObserver不能被實例化咸产,可以調(diào)用View.getViewTreeObserver()來獲得矢否。

ViewTreeObserver繼承關系:
public final class ViewTreeObserverextendsObject
java.lang.Object
android.view.ViewTreeObserver

ViewTreeObserver直接繼承自Object.

ViewTreeObserver提供了View的多種監(jiān)聽,每一種監(jiān)聽都有一個內(nèi)部類接口與之對應脑溢,內(nèi)部類接口全部保存在CopyOnWriteArrayList中僵朗,通過ViewTreeObserver.addXXXListener()來添加這些監(jiān)聽,源碼如下:

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數(shù)組中:

  /**
     * 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验庙,當視圖樹布局發(fā)生變化時不會再收到通知了:

/**
     * 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():視圖樹發(fā)生改變時通知觀察者,如果想在View Layout 或 View hierarchy 還未依附到Window時社牲,或者在View處于GONE狀態(tài)時強制布局粪薛,這個方法也可以手動調(diào)用。

   /**
     * 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狀態(tài)時強制繪制,可以手動調(diào)用這個方法痛阻。

 /**
     * 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常用內(nèi)部類:
內(nèi)部類接口 備注
ViewTreeObserver.OnPreDrawListener 當視圖樹將要被繪制時菌瘪,會調(diào)用的接口
ViewTreeObserver.OnGlobalLayoutListener 當視圖樹的布局發(fā)生改變或者View在視圖樹的可見狀態(tài)發(fā)生改變時會調(diào)用的接口
ViewTreeObserver.OnGlobalFocusChangeListener 當一個視圖樹的焦點狀態(tài)改變時,會調(diào)用的接口
ViewTreeObserver.OnScrollChangedListener 當視圖樹的一些組件發(fā)生滾動時會調(diào)用的接口
ViewTreeObserver.OnTouchModeChangeListener 當視圖樹的觸摸模式發(fā)生改變時阱当,會調(diào)用的接口
獲得View高度的幾種方式:

我們應該都遇到過在onCreate()方法里面調(diào)用view.getWidth()和view.getHeight()獲取到的view的寬高都是0的情況俏扩,這是因為在onCreate()里還沒有執(zhí)行測量,需要在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嫉戚,然后去調(diào)用onMeasure測量寬高,就可以得到寬高澈圈。

2彬檀、通過ViewTreeObserver .addOnGlobalLayoutListener來獲得寬高,當獲得正確的寬高后瞬女,請移除這個觀察者窍帝,否則回調(diào)會多次執(zhí)行:
//獲得ViewTreeObserver 
ViewTreeObserver observer=view.getViewTreeObserver();
//注冊觀察者,監(jiān)聽變化
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來獲得寬高,在執(zhí)行onDraw之前已經(jīng)執(zhí)行了onLayout()和onMeasure()报慕,可以得到寬高了深浮,當獲得正確的寬高后,請移除這個觀察者卖子,否則回調(diào)會多次執(zhí)行
//獲得ViewTreeObserver 
ViewTreeObserver observer=view.getViewTreeObserver();
//注冊觀察者略号,監(jiān)聽變化
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實現(xiàn)相應接口

public class MainActivity extends AppCompatActivity implements 
ViewTreeObserver.OnGlobalLayoutListener, 
ViewTreeObserver.OnPreDrawListener, ViewTreeObserver.OnGlobalFocusChangeListener, 
ViewTreeObserver.OnTouchModeChangeListener, View.OnClickListener

在onCreat中添加監(jiān)聽

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,實現(xiàn)接口方法

代碼

    @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

效果

這個接口很簡單就是監(jiān)聽focus的變化:

image.png

二.OnPreDrawListener

OnPreDrawListener接口是在繪制界面前調(diào)用

代碼
  @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 繼續(xù)繪制,返回false取消洋闽。
        return true;
    }

效果

image.png

如果返回false的話玄柠,效果是這樣的橱鹏,界面沒有繪制搭幻。


image.png

關于OnPreDrawListener的使用,有一個例子就是CoordinatorLayout調(diào)用Behavior的onDependentViewChanged就是通過注冊OnPreDrawListener接口序苏,在繪制的時候檢查界面是否發(fā)生變化刊懈,如果變化就調(diào)用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
        //分發(fā)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);
                }
            }
            //判斷是否發(fā)生變化
            // 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;
                    }
                //調(diào)用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.

當在一個視圖樹中全局布局發(fā)生改變或者視圖樹中的某個視圖的可視狀態(tài)發(fā)生改變時娃闲,所要調(diào)用的回調(diào)函數(shù)的接口類

代碼

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 隱藏");
                }
    }

效果

這里寫圖片描述

注意:在測試的時候發(fā)現(xiàn)使用

et_1.setVisibility(View.INVISIBLE);

時并不會觸發(fā)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);
        }
});
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末皇帮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蛋辈,更是在濱河造成了極大的恐慌属拾,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冷溶,死亡現(xiàn)場離奇詭異渐白,居然都是意外死亡,警方通過查閱死者的電腦和手機逞频,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門纯衍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人苗胀,你說我怎么就攤上這事襟诸。” “怎么了基协?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵励堡,是天一觀的道長。 經(jīng)常有香客問我堡掏,道長应结,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任泉唁,我火速辦了婚禮鹅龄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亭畜。我一直安慰自己扮休,他們只是感情好,可當我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布拴鸵。 她就那樣靜靜地躺著玷坠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪劲藐。 梳的紋絲不亂的頭發(fā)上八堡,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天,我揣著相機與錄音聘芜,去河邊找鬼兄渺。 笑死,一個胖子當著我的面吹牛汰现,可吹牛的內(nèi)容都是我干的挂谍。 我是一名探鬼主播叔壤,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼口叙!你這毒婦竟也來了炼绘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤妄田,失蹤者是張志新(化名)和其女友劉穎饭望,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體形庭,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年厌漂,在試婚紗的時候發(fā)現(xiàn)自己被綠了萨醒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡苇倡,死狀恐怖富纸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旨椒,我是刑警寧澤晓褪,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站综慎,受9級特大地震影響涣仿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜示惊,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一好港、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧米罚,春花似錦钧汹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至隘竭,卻和暖如春塘秦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背动看。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工嗤形, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弧圆。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓赋兵,卻偏偏與公主長得像笔咽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子霹期,可洞房花燭夜當晚...
    茶點故事閱讀 43,587評論 2 350

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,811評論 25 707
  • 1: 獲取控件寬高 控件View有getHeight()和getwidth()方法可以獲取寬高叶组,但是如果直接在on...
    自由人是工程師閱讀 1,773評論 0 0
  • 知道會有離別甩十,沒想到這么快。 很多時候我們用一種麻木的態(tài)度在生活吭产,就像早上看到乞討的人一樣侣监,那種淡然的態(tài)度讓人心里...
    養(yǎng)貓人依若閱讀 218評論 0 0
  • Commons Sense: Saving the Seas ① Mostly belonging to no o...
    新心斷點閱讀 96評論 0 0
  • 我說你 不要等待 想去就去 眼前的目光璀璨 不如遠方的一顫 走吧 看一看天邊的云彩 聽一聽戈壁的清籟 品一品紅茶的...
    半夏無塵閱讀 253評論 0 3