?Window表示一個窗口的概念,在日常開發中直接接觸Window的機會并不多,但卻會經常用到Window,activity
、toast
、dialog
、PopupWindow
、狀態欄等都是Window。在Android中Window是個抽象類,并且僅有一個實現類PhoneWindow
。
1、Window
?Android中,Window有應用Window、子Window及系統Window三種類型,分別對應不同的層級范圍,層級越高,顯示越靠前,這里的“靠前”是指層級大的Window會覆蓋在層級小的Window上面。
- 應用Window:對應層級范圍是1~99,每個activity就對應一個應用Window,如果在activity中創建了一個應用Window,那么當跳轉到另外一個Activity時,該Window會被覆蓋。應用Window的高度不受狀態欄影響。
- 子Window:對應層級范圍是1000~1999,PopupWindow默認就是一個子Window(可以修改PopupWindow的Window類型),如果在activity中創建了一個子Window,那么當跳轉到另外一個Activity時,該Window也會被覆蓋。子Window的高度受狀態欄影響。
- 系統Window:對應層級范圍是2000~2999,
toast
、狀態欄等都是系統Window,如果創建了一個系統Window,那么只有當該應用被銷毀時,該Window才被會關閉(排除主動關閉),所以可以用系統Window實現像360那樣的懸浮小球。系統Window需要設置<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
權限,否則會拋異常,在6.0以上需要動態申請。系統Window的高度不受狀態欄影響。
?前面說了Window的層級,下面就來看一個示例。
//代碼參考了PopupWindow的源代碼。private void startWindow() {//拿到activity中的wm對象,在attach中創建,是一個WindowManagerImpl對象wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);frame = new PopupDecorView(this);frame.setLayoutParams(new ActivityzhoLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));View view = View.inflate(this, R.layout.window_layout, null);Button bt = view.findViewById(R.id.window_layout_button);bt.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {dismiss();}});//重新設置WindowManager.LayoutParams的值WindowManager.LayoutParams p = createPopupLayoutParams(frame.getWindowToken());frame.addView(view);wm.addView(frame, p);}private LayoutParams createPopupLayoutParams(IBinder windowToken) {final WindowManager.LayoutParams p = new WindowManager.LayoutParams();//設置Window gravity。gravity 表示居中,top表示位于頂部p.gravity = Gravity.CENTER|Gravity.TOP;p.flags = computeFlags(p.flags);//設置Window的類型,其實這里我們也可以設置1~99、1000~1999、2000~2999之間的任意數字p.type = LayoutParams.TYPE_APPLICATION;//設置Window Tokenp.token = windowToken;//設置輸入法模式p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;//設置Window動畫p.windowAnimations = 0;//設置Window像素格式p.format = PixelFormat.TRANSLUCENT;// Used for debugging.p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));//設置Window寬p.width = LayoutParams.MATCH_PARENT;//設置Window高p.height = LayoutParams.WRAP_CONTENT;return p;}private int computeFlags(int curFlags) {curFlags &= ~(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;return curFlags;}//關閉Windowprivate void dismiss() {wm.removeView(frame);}private class PopupDecorView extends FrameLayout {public PopupDecorView(Context context) {super(context);}@Overridepublic boolean dispatchKeyEvent(KeyEvent event) {if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {if (getKeyDispatcherState() == null) {return super.dispatchKeyEvent(event);}if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {final KeyEvent.DispatcherState state = getKeyDispatcherState();if (state != null) {state.startTracking(event, this);}return true;} else if (event.getAction() == KeyEvent.ACTION_UP) {final KeyEvent.DispatcherState state = getKeyDispatcherState();if (state != null && state.isTracking(event) && !event.isCanceled()) {dismiss();return true;}}return super.dispatchKeyEvent(event);} else {return super.dispatchKeyEvent(event);}}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {
// if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
// return true;
// }return super.dispatchTouchEvent(ev);}}
復制代碼
?WindowManager.LayoutParams
用于描述Window的參數,關于其詳細參數可以參考Android Activity應用窗口的創建過程分析這篇文章。 ?先來看WindowManager.LayoutParams
的參數type
,也就是Window類型。下面來看不同Window類型的顯示效果
?粉色部分就是創建的Window,可以看出系統及應用Window不受狀態欄影響,而子Window卻因為狀態欄導致按鈕超出Window范圍。所以可以認為子Window的高度被被狀態欄占去一部分,而其他類型Window則不受此影響,讓WIndow居中時,子Window在手機中的位置也會比其他類型Window的位置高一些,這里就不驗證了,至于子Window為什么在狀態欄的下面,那是因為狀態欄的層級比子Window層級要高。
?WindowManager.LayoutParams
的flags
也是一個非常重要的參數,由于類型比較多,這里就主要介紹以下幾個類型。
- FLAG_NOT_TOUCH_MODAL:在此模式下,系統會將當前Window區域以外的單擊事件傳遞給底層的Window,當前Window區域內的單擊事件則自己處理。一般都需要開啟此標記
- FLAG_NOT_FOCUSABLE:在此模式下,Window不能獲取焦點,也不能接受各種輸入事件,此標記會同時開啟FLAG_NOT_TOUCH_MODAL,最終事件會直接傳遞給下層的具有焦點的Window。所以如果Window中有EditText等輸入控件時,就不應該啟用此標記。
- FLAG_SHOW_WHEN_LOCKED:開啟此模式可以讓Window顯示在鎖屏的界面。
?WindowManager.LayoutParams
中比較常用的參數就上面兩個,當然也可以設置Window的寬高、動畫、token等等,這里就不一一敘述了。 ?從上面示例可以看出,Window并不實際存在,它是以一個View的形式展示在屏幕上。
2、WindowManager
?WindowManager
的主要功能是提供簡單的API使得使用者可以方便地將一個View作為一個窗口添加到系統中,它是一個接口,繼承自ViewManager
接口,ViewManager
接口比較簡單,只有以下三個方法。
public interface ViewManager
{public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}
復制代碼
?從方法名也可以看出對Window的增刪改就是針對View的增刪改。方法雖然只有三個,但已經完全夠用了。WindowManager
的具體實現是WindowManagerImpl
。
public final class WindowManagerImpl implements WindowManager {private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();private final Context mContext;//父Windowprivate final Window mParentWindow;private IBinder mDefaultToken;...//添加View@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);}//更新View@Overridepublic void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);mGlobal.updateViewLayout(view, params);}...//異步移除View@Overridepublic void removeView(View view) {mGlobal.removeView(view, false);}//同步移除View@Overridepublic void removeViewImmediate(View view) {mGlobal.removeView(view, true);}...
}
復制代碼
?這里采用了代理模式,將所有操作交給WindowManagerGlobal
來執行。首先來看Window的添加。
2.1、添加Window
?在前面的例子中可以看到,創建一個Window就是向WindowManagerImpl
中添加一個View,而WindowManagerImpl
又將操作交給了WindowManagerGlobal
來處理,下面就來看看WindowManagerGlobal
中addView
的實現。
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {//檢查參數if (view == null) {throw new IllegalArgumentException("view must not be null");}if (display == null) {throw new IllegalArgumentException("display must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}//拿到Window的寬高、type等布局參數final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;...ViewRootImpl root;View panelParentView = null;synchronized (mLock) {...//查找View是否已經存在,WindowManager不允許同一個View被添加兩次int index = findViewLocked(view, false);if (index >= 0) {//如果View已在被銷毀的列表中,那么就先銷毀列表中存在的Viewif (mDyingViews.contains(view)) {// Don't wait for MSG_DIE to make it's way through root's queue.mRoots.get(index).doDie();} else {//很常見的一個異常,表示不能重復添加同一Viewthrow new IllegalStateException("View " + view+ " has already been added to the window manager.");}// The previous removeView() had not completed executing. Now it has.}//如果是子Window則需要先找到它的父Viewif (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {final int count = mViews.size();for (int i = 0; i < count; i++) {if (mRoots.get(i).mWindow.asBinder() == wparams.token) {panelParentView = mViews.get(i);}}}//創建一個新的ViewRootImplroot = new ViewRootImpl(view.getContext(), display);//給View設置參數view.setLayoutParams(wparams);//保存ViewmViews.add(view);//保存ViewRootImplmRoots.add(root);//保存參數mParams.add(wparams);//繪制View、添加Windowtry {// 將作為窗口的控件設置給ViewRootImpl。這個動作將導致ViewRootImpl向WMS添加新的窗口、申請Surface以及托管控件在Surface上的重繪動作。這才是真正意義上完成了窗口的添加操作root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.if (index >= 0) {removeViewLocked(index, true);}throw e;}}}
復制代碼
?在addView
方法中主要做了參數檢查、查找子Window的父View、創建ViewRootImpl
對象并通過ViewRootImpl
的setView
方法來實現View的繪制及Window添加操作。下面來看ViewRootImpl
中setView
方法的實現。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {//保存當前ViewmView = view;...//保存參數attrs = mWindowAttributes;...//繪制View。requestLayout();...try {...res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mInputChannel);} catch (RemoteException e) {....} finally {if (restore) {attrs.restore();}}...//添加失敗if (res < WindowManagerGlobal.ADD_OKAY) {mAttachInfo.mRootView = null;//添加失敗mAdded = false;mFallbackEventHandler.setView(null);unscheduleTraversals();setAccessibilityFocus(null, null);//返回錯誤的原因,相比很多錯誤信息大家都會遇到過switch (res) {//token出錯case WindowManagerGlobal.ADD_BAD_APP_TOKEN:case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:throw new WindowManager.BadTokenException("Unable to add window -- token " + attrs.token+ " is not valid; is your activity running?");case WindowManagerGlobal.ADD_NOT_APP_TOKEN:throw new WindowManager.BadTokenException("Unable to add window -- token " + attrs.token+ " is not for an application");case WindowManagerGlobal.ADD_APP_EXITING:throw new WindowManager.BadTokenException("Unable to add window -- app for token " + attrs.token+ " is exiting");//添加Window已存在case WindowManagerGlobal.ADD_DUPLICATE_ADD:throw new WindowManager.BadTokenException("Unable to add window -- window " + mWindow+ " has already been added");case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:// Silently ignore -- we would have just removed it// right away, anyway.return;case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:throw new WindowManager.BadTokenException("Unable to add window "+ mWindow + " -- another window of type "+ mWindowAttributes.type + " already exists");//未申請權限,當創建系統Window時是需要申請權限的case WindowManagerGlobal.ADD_PERMISSION_DENIED:throw new WindowManager.BadTokenException("Unable to add window "+ mWindow + " -- permission denied for window type "+ mWindowAttributes.type);case WindowManagerGlobal.ADD_INVALID_DISPLAY:throw new WindowManager.InvalidDisplayException("Unable to add window "+ mWindow + " -- the specified display can not be found");//window類型未在1~99,1000~1999,2000~2999這個范圍內。case WindowManagerGlobal.ADD_INVALID_TYPE:throw new WindowManager.InvalidDisplayException("Unable to add window "+ mWindow + " -- the specified window type "+ mWindowAttributes.type + " is not valid");}throw new RuntimeException("Unable to add window -- unknown error code " + res);}...}}}
復制代碼
?該方法真正意義上完成了View的繪制及Window的添加操作,來看requestLayout
與mWindowSession.addToDisplay
這兩個方法。前者主要是申請Surface以及托管控件在Surface上的重繪動作,即View的測量、布局、繪制流程。關于該方法詳細內容可以參考Android源碼分析之View繪制流程、《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系統這兩篇文章。后者主要向WindowManagerService(WMS)
添加新的窗口。 ?總體來說,WindowManagerGlobal
通過父窗口調整了布局參數之后,將新建的ViewRootImpl
、控件以及布局參數保存在mRoots
,mViews
及mParams
這三個數組中,然后將View交給新建的ViewRootImpl
進行處理,從而完成了窗口的添加。 ?WindowManagerGlobal
管理窗口的原理如下圖所示。
2.2、更新Window
?相對于添加Window,更新Window就簡單很多了,主要是修改布局參數,然后調用ViewRootImpl.setLayoutParams
來更新View。
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {if (view == null) {throw new IllegalArgumentException("view must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;//修改view的布局參數view.setLayoutParams(wparams);synchronized (mLock) {int index = findViewLocked(view, true);//查找view對應的ViewRootImplViewRootImpl root = mRoots.get(index);//移除舊的布局參數mParams.remove(index);//添加新的布局參數mParams.add(index, wparams);//更新布局參數root.setLayoutParams(wparams, false);}}復制代碼
?代碼還是比較簡單的,下面就來看ViewRootImpl
中setLayoutParams
方法的實現。
void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {synchronized (this) {//修改布局參數的操作...//對View進行重新測量、布局、繪制mWindowAttributesChanged = true;scheduleTraversals();}}
復制代碼
?該方法也比較簡單,主要就是調用scheduleTraversals
方法來對View進行重新測量、布局及繪制。scheduleTraversals
在這里就不詳細講解了,在View的繪制流程中已經講解的很清楚了。 ?總體上來說,Window的更新操作就是對View的重新測量、布局及繪制。
2.2、關閉Window
?關閉Window調用的是WindowManagerGlobal
的removeView
方法。
public void removeView(View view, boolean immediate) {if (view == null) {throw new IllegalArgumentException("view must not be null");}synchronized (mLock) {int index = findViewLocked(view, true);View curView = mRoots.get(index).getView();removeViewLocked(index, immediate);if (curView == view) {return;}throw new IllegalStateException("Calling with view " + view+ " but the ViewAncestor is attached to " + curView);}}//移除Viewprivate void removeViewLocked(int index, boolean immediate) {ViewRootImpl root = mRoots.get(index);View view = root.getView();if (view != null) {//拿到輸入法管理InputMethodManager imm = InputMethodManager.getInstance();if (imm != null) {//關閉輸入法Windowimm.windowDismissed(mViews.get(index).getWindowToken());}}//返回true表示異步刪除,false表示同步刪除boolean deferred = root.die(immediate);if (view != null) {view.assignParent(null);if (deferred) {//異步刪除只是將view添加到mDyingViews這個集合即可。mDyingViews.add(view);}}}//該方法在ViewRootImpl中boolean die(boolean immediate) {//立即移除Viewif (immediate && !mIsInTraversal) {doDie();return false;}...//異步移除View,mHandler.sendEmptyMessage(MSG_DIE);return true;}
復制代碼
?最終還是通過ViewRootImpl
來實現的Window的關閉,immediate
為true
時則代表立即刪除當前Window的信息及資源釋放,否則異步執行。當異步移除View時,也是調用了ViewRootImpl
的doDie
方法,只不過異步需要排隊而已。
void doDie() {//如果在非UI線程則報錯checkThread();...synchronized (this) {if (mRemoved) {return;}mRemoved = true;if (mAdded) {//資源釋放dispatchDetachedFromWindow();}if (mAdded && !mFirst) {destroyHardwareRenderer();...}mAdded = false;}//從mRoots、mViews及mParams這三個數組中移除信息WindowManagerGlobal.getInstance().doRemoveView(this);}
復制代碼
?在該方法里主是調用dispatchDetachedFromWindow
進行資源釋放,在dispatchDetachedFromWindow
中會釋放Surface所占內存、從WMS中移除Window、停止動畫、線程等。最后刷新WindowManagerGlobal
中mRoots
、mViews
及mParams
這三個數組的數據。 ?當調用ViewRootImpl
的doDie
方法后,該ViewRootImpl
也就完成了自己的使命了,等待被GC回收。因此可以得出這樣一個結論:ViewRootImpl的生命從setView()開始,到die()結束。
3、總結
?到這里,相必對WIndow及WindowManager就有了較深入的了解,主要總結以下幾點。
- Window分為應用Window、子Window及系統Window,不同類型的Window對應著不同的層級范圍,層級越高,顯示越靠前。
- 子Window的高度受狀態欄的影響。而系統Window及應用Window則無此限制,所以實現一個子Window需要考慮狀態欄的高度
- 一個Window對應著一個
ViewRootImpl
,也就是說ViewRootImpl
與Window同生共死。 - Window的更新其實對View的重新執行測量、布局及繪制。
【參考資料】 《Android藝術探索》 Android Activity應用窗口的創建過程分析 Android Window 機制探索 《深入理解Android 卷III》第四章 深入理解WindowManagerService 《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系統 [深入理解Android卷一全文-第八章]深入理解Surface系統