Window表示一個(gè)窗口疚鲤,是View的實(shí)際管理者客年。在第4章的事件分發(fā)中已經(jīng)知道了屡谐,點(diǎn)擊事件是通過Window->DecorView->View來傳遞的氨鹏。
Window是一個(gè)抽象類颜屠,具體實(shí)現(xiàn)是PhoneWindow類辰妙。我們可以通過WindowManager來操作Window,具體實(shí)現(xiàn)是在WindowManagerService中實(shí)現(xiàn)的甫窟;WindowManager和WindowManagerService通過IPC連接密浑。
8.1 Window和WindowManager
8.1.1 創(chuàng)建一個(gè)Window
首先,通過WindowManager添加一個(gè)窗口:
Button button = new Button(this);
button.setText("button");
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION, 0,
PixelFormat.TRANSPARENT);
lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
lp.gravity = Gravity.LEFT | Gravity.TOP;
lp.x = 100;
lp.y = 300;
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
wm.addView(button, lp);
這里注意粗井,原書中代碼報(bào)錯(cuò)尔破,必須給窗口指定一個(gè)type,我這里指定的WindowManager.LayoutParams.TYPE_APPLICATION浇衬;不清楚是否是系統(tǒng)版本的問題懒构。
8.1.2 Type
Type有三種類型:
應(yīng)用Window
應(yīng)用類Window對(duì)應(yīng)一個(gè)Activity。取值在1~99之間耘擂。子Window
子Window附屬于一個(gè)特定的父Window胆剧,而不能單獨(dú)存在。取值在1000~1999之間醉冤。系統(tǒng)Window
系統(tǒng)Window需要系統(tǒng)權(quán)限才能創(chuàng)建秩霍。取值在2000~2999之間。
8.1.3 Flag
Flag參數(shù)表示W(wǎng)indow的屬性蚁阳,可以控制Window的顯示特性铃绒。
FLAG_NOT_FOCUSABLE
表示W(wǎng)indow不需要獲取焦點(diǎn),也不需要接受各種輸入事件螺捐。FLAG_NOT_TOUCH_MODAL
表示W(wǎng)indow會(huì)處理當(dāng)前區(qū)域以內(nèi)的單擊事件颠悬,同時(shí)把當(dāng)前Window區(qū)域以外的單擊事件傳遞到底層。FLAG_SHOW_WHEN_LOCKED
表示W(wǎng)indow可以顯示在鎖屏的界面上定血。
8.1.4 權(quán)限
想要顯示系統(tǒng)級(jí)的Window是需要申請(qǐng)權(quán)限的赔癌。例如當(dāng)type = TYPE_SYSTEM_ERROR的時(shí)候,需要在AndroidManifest中添加<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
澜沟。
在Android 6.0及以后届榄,需要手動(dòng)開啟懸浮窗權(quán)限:
/**
* 檢查是否有懸浮窗權(quán)限
*/
@TargetApi(Build.VERSION_CODES.M)
private void checkOverlay() {
if (!Settings.canDrawOverlays(this)) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("權(quán)限禁止")
.setMessage("懸浮窗權(quán)限被禁止,點(diǎn)擊確定前往設(shè)置")
.setPositiveButton("確定",
(dialog1, which) -> jumpToOverlaySetting())
.setCancelable(false)
.create();
dialog.show();
}
}
private void jumpToOverlaySetting() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivityForResult(intent, 1);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
checkOverlay();
}
}
然后在onActivityResult()中倔喂,再判斷是否開啟權(quán)限铝条。
如果不想申請(qǐng)權(quán)限靖苇,使用type = TYPE_TOAST也可以創(chuàng)建一個(gè)懸浮窗。但是在Android 7.1.1(API 25)開始班缰,TYPE_TOAST被限制使用[1]贤壁。如果想要在7.1.1以后使用懸浮窗,必須開啟權(quán)限埠忘。
8.2 Window的內(nèi)部機(jī)制
Window是一個(gè)抽象概念脾拆,而不是實(shí)際存在的。每一個(gè)Window都對(duì)應(yīng)著一個(gè)View和一個(gè)ViewRootImpl莹妒,Window和View是通過ViewRootImpl來建立聯(lián)系的名船。Window實(shí)際存在的形式是View。
對(duì)Window的操作只能通過WindowManager來進(jìn)行旨怠。WindowManager主要提供了三個(gè)方法:
- addView(View, LayoutParams)
- removeView(View)
- updateView(View, LayoutParams)
可以看到渠驼,WindowManager的操作對(duì)象是View。
8.2.1 Window的添加過程
在WindowManager的實(shí)現(xiàn)類WindowManagerImpl中鉴腻,找到addView()方法:
public void addView(View view, ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
而mGlobal的定義:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
這個(gè)單例類迷扇,在第四章[2]的時(shí)候已經(jīng)接觸過了。這里我們看到爽哎,WindowManager的添加蜓席、刪除、更新三個(gè)方法课锌,其實(shí)都全權(quán)交給了WindowManagerGlobal厨内。
轉(zhuǎn)到WindowManagerGlobal的addView()方法中:
public void addView(View view,
ViewGroup.LayoutParams params,
Display display,
Window parentWindow) {
// ... 省略若干代碼
// 創(chuàng)建ViewRoot
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// 將View添加到列表中
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
這里主要是將待添加的View、ViewRoot渺贤、LayoutParams保存到列表中雏胃,創(chuàng)建了ViewRoot,以及調(diào)用了ViewRoot的setView方法癣亚。
接下來繼續(xù)跟蹤setView方法丑掺。
public void setView(View view,
WindowManager.LayoutParams attrs,
View panelParentView) {
// ...省略若干代碼
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
// ...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
// ...
}
requestLayout()之前有一個(gè)注釋:
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
大概意思是說获印,為了保證在接收到其他系統(tǒng)事件之前布局好述雾,我們?cè)趯iew添加到Window之前進(jìn)行第一次的布局。
然后調(diào)用了WindowSession.addToDisplay()方法兼丰。mWindowSession是通過WindowManagerGlobal.getWindowSession()來獲取的玻孟,而跟蹤進(jìn)去發(fā)現(xiàn)發(fā)現(xiàn)這是一個(gè)Binder對(duì)象,進(jìn)行了和WindowManagerService之間的IPC鳍征。也就是說黍翎,Window的添加最后交給了WindowManagerService來進(jìn)行。
8.2.2 Window的刪除過程
Window的刪除的過程和添加類似艳丛,通過WindowManager.removeView() -> ViewRoot.die() -> WindowSession.remove()最終交付WindowManagerService來處理匣掸。
在這個(gè)過程中趟紊,會(huì)調(diào)用View的onDetachedFromWindow()回調(diào)。然后會(huì)調(diào)用WindowManager.doRemoveView()來將之前添加到列表中的View碰酝、ViewRoot霎匈、LayoutParams(8.2.1)給刪掉。
8.2.3 Window的更新過程
更新過程相較添加和刪除比較簡單送爸,WindowManager.updateViewLayout() -> ViewRoot.setLayoutParams() -> ViewRoot.scheduleTraversals()
最終會(huì)調(diào)用ViewRoot.performTraversals()方法铛嘱,在這里會(huì)對(duì)View進(jìn)行measure、layout袭厂、draw的步驟墨吓。同時(shí)會(huì)調(diào)用relayoutWindow -> WindowSession.relayoutWindow(),通過WindowSession來更新Window視圖纹磺,這個(gè)過程仍然是經(jīng)過IPC帖烘,由WindowManagerService來實(shí)現(xiàn)的。
8.2.* 小結(jié)
- Window是一個(gè)抽象概念爽航,實(shí)際上是不存在的蚓让,通過View來體現(xiàn)。View不能單獨(dú)存在讥珍,而必須依托于Window历极;而Window的具體呈現(xiàn)方式則是通過View。
- 一個(gè)Window對(duì)應(yīng)一個(gè)ViewRoot對(duì)應(yīng)一個(gè)View衷佃,ViewRoot是WindowManager和View之間的紐帶趟卸。而ViewRoot所代表的意義,則是這個(gè)Window的ViewTree的根節(jié)點(diǎn)氏义。
WindowManager ----> ViewRoot --(IPC)-> WindowManagerService 這是操作Window的一般流程锄列。而所有關(guān)于Window的操作,最終都是在WindowManagerService中實(shí)現(xiàn)的惯悠。
8.3 Window的創(chuàng)建過程
8.3.1 Activity的Window
在第四章里面邻邮,我分析過Activity的啟動(dòng)流程:第4章 View的工作原理。
當(dāng)系統(tǒng)將要啟動(dòng)一個(gè)Activity的時(shí)候克婶,會(huì)調(diào)用ActivityThread.performLaunchActivity()方法筒严,在這個(gè)方法中會(huì)創(chuàng)建Activity實(shí)例,并調(diào)用Activity的attach方法情萤。在attach方法中鸭蛙,會(huì)賦予Activity上下文,創(chuàng)建Window并將其和Activity綁定筋岛。Activity實(shí)現(xiàn)了Window.Callback接口娶视,所以Activity可以收到Window的若干回調(diào),比如onAttachedToWindow睁宰、onDetachedFromWindow肪获、dispatchTouchEvent等寝凌。
Window實(shí)例的創(chuàng)建,是通過PolicyManager的工廠方法makeNewWindow(Context)來進(jìn)行的孝赫。
當(dāng)我們?cè)贏ctivity調(diào)用setContentView方法時(shí)硫兰,會(huì)創(chuàng)建DecorView并將xml的布局加載進(jìn)content中。DecorView雖然已經(jīng)創(chuàng)建完畢寒锚,但是他還沒有被WindowManager添加到Window中劫映。Window的概念,更多表示的是一種抽象的功能集合刹前。在Window和DecorView都創(chuàng)建完畢之后泳赋,ActivityThread會(huì)調(diào)用handleResumeActivity方法->makeVisible方法,在其中會(huì)調(diào)用WindowManager.addView()喇喉,這就回到了8.2.1的流程了祖今,通過addView將DecorView添加到Window。
(有點(diǎn)吃驚拣技,書中的內(nèi)容和我之前在第四章中分析的幾乎一模一樣千诬,不過這也說明我沒有誤入歧途。)
8.3.2 Dialog的Window
Dialog和Activity創(chuàng)建Window的方式類似膏斤。
1徐绑、創(chuàng)建Window
2、初始化DecorView并將Dialog的視圖添加進(jìn)去
3莫辨、將DecorView添加到Window中
在Dialog被關(guān)閉時(shí)傲茄,他會(huì)通過WindowManager來移除DecorView。
8.3.3 Toast的Window
Toast也是基于Window來實(shí)現(xiàn)的沮榜,不過過程和Dialog不同盘榨。
總的來說,Toast是通過IPC過程調(diào)用NotificationManagerService來進(jìn)行顯示和隱藏的蟆融。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}