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.
     */
    @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;
     }
   });

案例分析

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);
        }
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,520评论 25 707
  • 1: 获取控件宽高 控件View有getHeight()和getwidth()方法可以获取宽高,但是如果直接在on...
    自由人是工程师阅读 1,767评论 0 0
  • 知道会有离别,没想到这么快。 很多时候我们用一种麻木的态度在生活,就像早上看到乞讨的人一样,那种淡然的态度让人心里...
    养猫人依若阅读 213评论 0 0
  • Commons Sense: Saving the Seas ① Mostly belonging to no o...
    新心断点阅读 95评论 0 0
  • 我说你 不要等待 想去就去 眼前的目光璀璨 不如远方的一颤 走吧 看一看天边的云彩 听一听戈壁的清籁 品一品红茶的...
    半夏无尘阅读 248评论 0 3