在Android開發(fā)過程中鸟廓,經(jīng)常需要獲取Window或某個View的可見性變化時機从祝,以便在View的Visibility變化時進行相應(yīng)的處理。目前引谜,比較常用的判斷View可見性時機的回調(diào)有
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();
}
}
由方法注釋可知,它是在窗口可見性改變時調(diào)用员咽,而且注意這只是在Window對WindowManager可見時調(diào)用毒涧,并不是告知你當前可見的Window是否被遮擋。
查看代碼贝室,發(fā)現(xiàn)其調(diào)用位置有3處契讲,添加時在performTraversals
方法中(代碼有省略)仿吞,Activity onStop生命周期會remove掉DecorView和對應(yīng)的Window,在removeView
方法中會調(diào)用dispatchDetachedFromWindow
方法怀泊,該方法內(nèi)又會調(diào)用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);
}
}
......
}
調(diào)用位置已注釋茫藏,首先看第一處①,應(yīng)用啟動時霹琼,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] 調(diào)用自身的dispatchAttachedToWindow方法,處理自己attach到Window
[2] 向子View分發(fā)事件忠藤,讓每個子View處理attach到Window的事件
由此可知挟伙,最終都會調(diào)用到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
方法集中處理了onAttachedToWindow
、onViewAttachedToWindow
模孩、onWindowVisibilityChanged
和onVisibilityChanged
這4種可見性變化相關(guān)的回調(diào)函數(shù)尖阔。
對于onWindowVisibilityChanged
方法來說,首先會通過AttachInfo對象獲取現(xiàn)在窗口(mWindowVisibility)可見性榨咐。mWindowVisibility
變量的賦值也在performTraversals方法中介却。
......
final int viewVisibility = getHostVisibility();
// mViewVisibility在創(chuàng)建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
就會調(diào)用onWindowVisibilityChanged
方法数焊。這就是它的第一種調(diào)用場景永淌。
查看getHostVisibility()方法
// #ViewRootImpl.java
int getHostVisibility() {
return (mAppVisible || mForceDecorViewVisibility) ? mView.getVisibility() : View.GONE;
}
可見,最終取得的是mView對象的可見性佩耳,而mView對象就是DecorView對象(在ViewRootImpl#setView方法中設(shè)置的)遂蛀,所以:
mWindowVisibility
方法只會在頁面(Activity和Dialog)打開和關(guān)閉(具體說是Activity或Dialog的Window可見性改變時)各調(diào)用一次
第二處調(diào)用位置在②處,主要代碼是
if (viewVisibilityChanged) {
mAttachInfo.mWindowVisibility = viewVisibility;
/**
* ②
*/
host.dispatchWindowVisibilityChanged(viewVisibility);
......
}
在DecorView加載時干厚,如果mFirst==false
(非首次加載)李滴,很可能進行該條件體進行調(diào)用。
第三種情況③萍诱,例如打開一個新頁面悬嗓,老頁面走到onStop聲明周期方法,如③處裕坊,只要不是GONE就會調(diào)用包竹。
綜上,onWindowVisibilityChanged
的調(diào)用:
每當一個頁面打開或移除時(具體點說就是ViewRootImpl將DecovView添加到屏幕上展示),如果關(guān)聯(lián)的Window可見(不等于GONE)周瞎,則會調(diào)用
打開時苗缩,是在
View#dispatchAttachedToWindow
中進行調(diào)用,分離時在View#dispatchDetachFromWindow
時調(diào)用声诸,并傳入默認參數(shù)GONE
二酱讶、onVisibilityChanged
onVisibilityChanged
調(diào)用時機和onWindowVisibilityChanged
非常類似,對于APP啟動打開頁面時彼乌,會處理重寫該方法的View attach到Window的事件泻肯,此時默認傳入的參數(shù)是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);
......
}
然后慰照,每次調(diào)用setVisibility
方法來控制視圖的可見性時都會回調(diào)該方法灶挟。
// 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);
}
同樣,在頁面關(guān)閉時(或打開新頁面覆蓋舊頁面)毒租,會執(zhí)行onStop生命周期方法稚铣,其實是調(diào)用ActivityThread#handleStopActivity
方法,然后會調(diào)用到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惕医,此時mVisibleFromServer
為true,進入條件體算色,首先將mVisibleFromServer
設(shè)置為false抬伺,然后通過DecorView調(diào)用setVisibility
方法來控制視圖顯示,并默認傳輸View.INVISIBLE
剃允。我們知道調(diào)用setVisibility
就可能觸發(fā)onVisibilityChanged
的執(zhí)行沛简。
總結(jié):
頁面加載時齐鲤,會在View attach到Window時(dispatchAttachedToWindow方法)調(diào)用
onVisibilityChanged
通過
setVisibility
來改變View的可見性時會調(diào)用onVisibilityChanged
關(guān)閉頁面時斥废,在
handleStopActivity
方法中會調(diào)用updateVisibility
方法,內(nèi)部也會調(diào)用setVisibility
给郊,并傳入默認參數(shù)INVISIBLE牡肉。
三、OnAttachStateChangeListener#onViewAttachedToWindow
OnAttachStateChangeListener定義了2個接口方法淆九,分別在View Attach/Detach to Window時候調(diào)用统锤。要使用該接口首先需要注冊這個監(jiān)聽器。
public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
ListenerInfo li = getListenerInfo();
if (li.mOnAttachStateChangeListeners == null) {
li.mOnAttachStateChangeListeners
= new CopyOnWriteArrayList<OnAttachStateChangeListener>();
}
li.mOnAttachStateChangeListeners.add(listener);
}
調(diào)用位置很單純炭庙,就在View#dispatchAttachedToWindow方法里饲窿,如果有注冊過該監(jiān)聽器,就會調(diào)用
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);
}
}
......
}