Android OnWindowVisibilityChanged、OnVisibilityChanged和OnViewAttachedToWindow详解

在Android开发过程中,经常需要获取Window或某个View的可见性变化时机,以便在View的Visibility变化时进行相应的处理。目前,比较常用的判断View可见性时机的回调有

  • onWindowVisibilityChanged

  • onVisibilityChanged

  • OnAttachStateChangeListener#onViewAttachedToWindow

一、onWindowVisibilityChanged

/**
 * Called when the window containing has change its visibility
 * (between {@link #GONE}, {@link #INVISIBLE}, and {@link #VISIBLE}).  Note
 * that this tells you whether or not your window is being made visible
 * to the window manager; this does <em>not</em> tell you whether or not
 * your window is obscured by other windows on the screen, even if it
 * is itself visible.
 *
 * @param visibility The new visibility of the window.
 */
 protected void onWindowVisibilityChanged(@Visibility int visibility) {
   if (visibility == VISIBLE) {
     initialAwakenScrollBars();
   }
 }

由方法注释可知,它是在窗口可见性改变时调用,而且注意这只是在WindowWindowManager可见时调用,并不是告知你当前可见的Window是否被遮挡。

查看代码,发现其调用位置有3处,添加时在performTraversals方法中(代码有省略),Activity onStop生命周期会removeDecorView和对应的Window,在removeView方法中会调用dispatchDetachedFromWindow方法,该方法内又会调用onWindowVisibilityChanged

private void performTraversals() {
 // cache mView since it is used so much below...
 final View host = mView;
 ......

 final int viewVisibility = getHostVisibility();
 final boolean viewVisibilityChanged = !mFirst && (mViewVisibility != viewVisibility|| mNewSurfaceNeeded
 // Also check for possible double visibility update, which will make current
 // viewVisibility value equal to mViewVisibility and we may miss it.
 || mAppVisibilityChanged);
 mAppVisibilityChanged = false;
 final boolean viewUserVisibilityChanged = !mFirst &&
 ((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE));

 ......
 if (mFirst) {
 ......

 // We used to use the following condition to choose 32 bits drawing caches:
 // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
 // However, windows are now always 32 bits by default, so choose 32 bits
 mAttachInfo.mUse32BitDrawingCache = true;
 mAttachInfo.mWindowVisibility = viewVisibility;
 mAttachInfo.mRecomputeGlobalAttributes = false;
 mLastConfigurationFromResources.setTo(config);
 mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
 // Set the layout direction if it has not been set before (inherit is the default)
 if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
   host.setLayoutDirection(config.getLayoutDirection());
 }
 /**
 *  ①
 */
 host.dispatchAttachedToWindow(mAttachInfo, 0);
 mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
 dispatchApplyInsets(host);
 } else {
   ......
   }
 }

 if (viewVisibilityChanged) {
   mAttachInfo.mWindowVisibility = viewVisibility;
   /**
   *  ②
   */
   host.dispatchWindowVisibilityChanged(viewVisibility);
   if (viewUserVisibilityChanged) {
     host.dispatchVisibilityAggregated(viewVisibility == View.VISIBLE);
   }
   if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
     endDragResizing();
     destroyHardwareResources();
   }
   if (viewVisibility == View.GONE) {
     // After making a window gone, we will count it as being
     // shown for the first time the next time it gets focus.
     mHasHadWindowFocus = false;
    }
  }
}
void dispatchDetachedFromWindow() {
 AttachInfo info = mAttachInfo;
 if (info != null) {
   int vis = info.mWindowVisibility;
   if (vis != GONE) {
     onWindowVisibilityChanged(GONE);// ③
   if (isShown()) {
     // Invoking onVisibilityAggregated directly here since the subtree
     // will also receive detached from window
     onVisibilityAggregated(false);
   }
 }
}

 onDetachedFromWindow();
 onDetachedFromWindowInternal();

 ......
 ListenerInfo li = mListenerInfo;
 final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
 li != null ? li.mOnAttachStateChangeListeners : null;
 if (listeners != null && listeners.size() > 0) {
 // 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.
 for (OnAttachStateChangeListener listener : listeners) {
   listener.onViewDetachedFromWindow(this);
  }
 }

 ......
 }

调用位置已注释,首先看第一处①,应用启动时,host就是DecorView对象,它是一个ViewGroup,然后在ViewGroup类中查看。

/**
 * ViewGroup.class
 */
 @Override
 @UnsupportedAppUsage
 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
   mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
   // [1]
   super.dispatchAttachedToWindow(info, visibility);
   mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

   final int count = mChildrenCount;
   final View[] children = mChildren;
   for (int i = 0; i < count; i++) {
     final View child = children[i];
     // [2]
     child.dispatchAttachedToWindow(info,
     combineVisibility(visibility, child.getVisibility()));
   }
   final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
   for (int i = 0; i < transientCount; ++i) {
     View view = mTransientViews.get(i);
     view.dispatchAttachedToWindow(info,
     combineVisibility(visibility, view.getVisibility()));
   }
 }

可以看到,ViewGroup#dispatchAttachedToWindow方法主要作用就是

[1] 调用自身的dispatchAttachedToWindow方法,处理自己attachWindow

[2] 向子View分发事件,让每个子View处理attachWindow的事件

由此可知,最终都会调用到View#dispatchAttachedToWindow方法:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
 mAttachInfo = info;
 ......
 // Transfer all pending runnables.
 if (mRunQueue != null) {
 mRunQueue.executeActions(info.mHandler);
 mRunQueue = null;
 }
 performCollectViewAttributes(mAttachInfo, visibility);
 onAttachedToWindow();// [1]

 ListenerInfo li = mListenerInfo;
 final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
 li != null ? li.mOnAttachStateChangeListeners : null;
 if (listeners != null && listeners.size() > 0) {
 // 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.
 for (OnAttachStateChangeListener listener : listeners) {
     listener.onViewAttachedToWindow(this);// [2]
   }
 }

 int vis = info.mWindowVisibility;
 if (vis != GONE) {
     onWindowVisibilityChanged(vis);// [3]
     if (isShown()) {
     // Calling onVisibilityAggregated directly here since the subtree will also
     // receive dispatchAttachedToWindow and this same call
     onVisibilityAggregated(vis == VISIBLE);
   }
 }

 // Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
 // As all views in the subtree will already receive dispatchAttachedToWindow
 // traversing the subtree again here is not desired.
 onVisibilityChanged(this, visibility);

 ......
 }

View#dispatchAttachedToWindow方法集中处理了onAttachedToWindowonViewAttachedToWindowonWindowVisibilityChangedonVisibilityChanged这4种可见性变化相关的回调函数。

对于onWindowVisibilityChanged方法来说,首先会通过AttachInfo对象获取现在窗口(mWindowVisibility)可见性。mWindowVisibility变量的赋值也在performTraversals方法中。

......
final int viewVisibility = getHostVisibility();
// mViewVisibility在创建ViewRootImpl对象时,初始化值是GONE;在完成测量布局后赋值为viewVisibility
final boolean viewVisibilityChanged = !mFirst
 && (mViewVisibility != viewVisibility || mNewSurfaceNeeded
 // Also check for possible double visibility update, which will make current
 // viewVisibility value equal to mViewVisibility and we may miss it.
 || mAppVisibilityChanged);
......
mAttachInfo.mWindowVisibility = viewVisibility;
......

Window被添加到屏幕上后(mWindowSession.addToDisplay),getHostVisibility()就返回Visible。所以只要mWindowVisibility不为GONE就会调用onWindowVisibilityChanged方法。这就是它的第一种调用场景。
查看getHostVisibility()方法

// #ViewRootImpl.java
int getHostVisibility() {
        return (mAppVisible || mForceDecorViewVisibility) ? mView.getVisibility() : View.GONE;
    }

可见,最终取得的是mView对象的可见性,而mView对象就是DecorView对象(在ViewRootImpl#setView方法中设置的),所以:

mWindowVisibility方法只会在页面(ActivityDialog)打开和关闭(具体说是ActivityDialogWindow可见性改变时)各调用一次
第二处调用位置在处,主要代码是

 if (viewVisibilityChanged) {
 mAttachInfo.mWindowVisibility = viewVisibility;
 /**
 *  ②
 */
 host.dispatchWindowVisibilityChanged(viewVisibility);
 ......
 }

DecorView加载时,如果mFirst==false(非首次加载),很可能进行该条件体进行调用。

第三种情况③,例如打开一个新页面,老页面走到onStop声明周期方法,如③处,只要不是GONE就会调用。

综上,onWindowVisibilityChanged的调用:

  • 每当一个页面打开或移除时(具体点说就是ViewRootImpl将DecovView添加到屏幕上展示),如果关联的Window可见(不等于GONE),则会调用

  • 打开时,是在View#dispatchAttachedToWindow中进行调用,分离时在View#dispatchDetachFromWindow时调用,并传入默认参数GONE

二、onVisibilityChanged

onVisibilityChanged调用时机和onWindowVisibilityChanged非常类似,对于APP启动打开页面时,会处理重写该方法的View attach到Window的事件,此时默认传入的参数是Visible(值为0)。

 // ViewRootImpl.class
 private void performTraversals() {
 ......
 host.dispatchAttachedToWindow(mAttachInfo, 0);
 ......
 }

 // View.class
 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
 ......
 // Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
 // As all views in the subtree will already receive dispatchAttachedToWindow
 // traversing the subtree again here is not desired.
 onVisibilityChanged(this, visibility);
 ......
 }

然后,每次调用setVisibility方法来控制视图的可见性时都会回调该方法。

    // View.class
    public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
    }

    void setFlags(int flags, int mask) {
        ......

        if ((changed & VISIBILITY_MASK) != 0) {
            ......
            if (mAttachInfo != null) {
                dispatchVisibilityChanged(this, newVisibility);
                ......
            }
        }        
        .......
    }

    protected void dispatchVisibilityChanged(@NonNull View changedView,
            @Visibility int visibility) {
        onVisibilityChanged(changedView, visibility);
    }

同样,在页面关闭时(或打开新页面覆盖旧页面),会执行onStop生命周期方法,其实是调用ActivityThread#handleStopActivity方法,然后会调用到updateVisibility方法

// ActivityThread.class
private void updateVisibility(ActivityClientRecord r, boolean show) {
        View v = r.activity.mDecor;
        if (v != null) {
            if (show) {
                if (!r.activity.mVisibleFromServer) {
                    r.activity.mVisibleFromServer = true;
                    mNumVisibleActivities++;
                    if (r.activity.mVisibleFromClient) {
                        r.activity.makeVisible();
                    }
                }
                ......
            } else {
                if (r.activity.mVisibleFromServer) {
                    r.activity.mVisibleFromServer = false;
                    mNumVisibleActivities--;
                    v.setVisibility(View.INVISIBLE);
                }
            }
        }
    }

mVisibleFromServer默认是false,在onResume后赋值为true,此时mVisibleFromServertrue,进入条件体,首先将mVisibleFromServer设置为false,然后通过DecorView调用setVisibility方法来控制视图显示,并默认传输View.INVISIBLE。我们知道调用setVisibility就可能触发onVisibilityChanged的执行。

总结

  • 页面加载时,会在View attachWindow时(dispatchAttachedToWindow方法)调用onVisibilityChanged

  • 通过setVisibility来改变View的可见性时会调用onVisibilityChanged

  • 关闭页面时,在handleStopActivity方法中会调用updateVisibility方法,内部也会调用setVisibility,并传入默认参数INVISIBLE

三、OnAttachStateChangeListener#onViewAttachedToWindow

OnAttachStateChangeListener定义了2个接口方法,分别在View Attach/Detach to Window时候调用。要使用该接口首先需要注册这个监听器。

public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
        ListenerInfo li = getListenerInfo();
        if (li.mOnAttachStateChangeListeners == null) {
            li.mOnAttachStateChangeListeners
                    = new CopyOnWriteArrayList<OnAttachStateChangeListener>();
        }
        li.mOnAttachStateChangeListeners.add(listener);
    }

调用位置很单纯,就在View#dispatchAttachedToWindow方法里,如果有注册过该监听器,就会调用

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        ......
        onAttachedToWindow();

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

推荐阅读更多精彩内容