Window和WindowManager
為了分析Window的工作機(jī)制,我們看下如何用 WindowManager 添加一個(gè) Window。
mFloatingButton = new Button(this);
mFloatingButton.setText("button");
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mLayoutParams = new WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL| LayoutParams.FLAG_NOT_FOCUSABLE| LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.type = LayoutParams.TYPE_SYSTEM_ALERT;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mWindowManager.addView(mFloatingButton, mLayoutParams);
以上代碼將一個(gè)Button添加到屏幕坐標(biāo)為(100,300)的位置上,要在Manifest.xml中添加權(quán)限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />。
WindowManager中的 flags、type 這兩個(gè)參數(shù)比較重要。
1. 幾個(gè)flag屬性
WindowManager.LayoutParams.FLAG_SECURE
不允許截屏;設(shè)置了這個(gè)屬性的窗口,在窗口可見的情況下,是會(huì)禁用系統(tǒng)的截圖功能的。那么問題來(lái)了:假如有一天,你的公司要求寫一個(gè)類似于‘閱后即焚’功能的頁(yè)面的話,不妨在activity中獲得WindowManager.LayoutParams并添加該屬性,輕輕松松搞定。WindowManager.LayoutParams.FLAG_BLUR_BEHIND
背景模糊;假如你的窗口設(shè)置了這個(gè)屬性,并且這個(gè)窗口可見,在這窗口之后的所有背景都會(huì)被模糊化,但我還沒有發(fā)現(xiàn)一個(gè)屬性是可以控制模糊程度的。WindowManager.LayoutParams.FLAG_DIM_BEHIND
背景變暗;設(shè)置這個(gè)效果的窗口,在窗口可見的情況下,窗口后方的背景會(huì)相應(yīng)的變暗,這個(gè)屬性需要配合參數(shù)dimAmount一起使用,dimAmount會(huì)在后文中介紹。WindowManager.LayoutParams.FLAG_FULLSCREEN
設(shè)置全屏;這個(gè)屬性也許是大家接觸的最多的一個(gè)屬性,很多應(yīng)用開發(fā)過(guò)程中會(huì)要求有些頁(yè)面需要?jiǎng)討B(tài)設(shè)置Activity為全屏,而我們只需要獲得Activity的WindowManager.LayoutParams并設(shè)置WindowManager.LayoutParams.FLAG_FULLSCREEN屬性就行。WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
設(shè)備常亮;設(shè)置這個(gè)屬性的窗口,在窗口可見的情況下,整個(gè)屏幕會(huì)處于常亮并且高亮度的狀態(tài),并且不受待機(jī)時(shí)間的約束。WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
布局不受限制;設(shè)置這個(gè)屬性的窗口,將不再受設(shè)備顯示范圍邊界 的約束,通俗點(diǎn)講,就是窗口可以出設(shè)備之外,然后移除部分不可見。具體會(huì)在坐標(biāo)參數(shù)中講到。WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
不設(shè)置聚焦;關(guān)于焦點(diǎn)獲得我有必要說(shuō)明一下,如果窗口獲得焦點(diǎn)的話,只要窗口處于可視化狀態(tài),當(dāng)前設(shè)備的物理按鍵點(diǎn)擊事件都會(huì)被這個(gè)窗口接收,但是如果不設(shè)置窗口的焦點(diǎn)的話,直接傳遞到之后窗口進(jìn)行接收。這就導(dǎo)致一個(gè)問題,如果你的需求要求你寫的懸浮窗點(diǎn)擊返回鍵能夠關(guān)閉或是進(jìn)行其他操作的話,你就必須讓你的窗口獲得焦點(diǎn),并為當(dāng)前View設(shè)置按鍵監(jiān)聽事件。WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
取消觸摸事件; 設(shè)置這個(gè)屬性的窗口將不再處理任何Touch事件,就算顯示的View設(shè)置了onTouch事件,那么這個(gè)窗口就會(huì)是一個(gè)僵尸窗口。WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
不知道怎么去歸納,這個(gè)屬性還是比較有意思的,設(shè)置這個(gè)屬性的窗口,在窗口可見的情況下,就算窗口沒有設(shè)置屬性FLAG_NOT_FOCUSABLE,也就是在窗口獲得焦點(diǎn)的情況下,當(dāng)觸摸事件是在窗口之外區(qū)域的時(shí)候,窗口不在攔截觸摸事件,而是將事件往下傳遞,也算是解決聚焦后的事件攔截問題吧。WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER
顯示壁紙;官方文檔說(shuō)明是在窗口之后顯示系統(tǒng)壁紙,但是我親測(cè),似乎并沒有這個(gè)想效果,還是這個(gè)屬性需要配合其他的屬性設(shè)置一起使用,希望有設(shè)置成功的小伙伴能夠在評(píng)論區(qū)分享你的結(jié)果。WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
鎖屏顯示;關(guān)于這個(gè)屬性官方文檔給出的說(shuō)明是在鎖屏的時(shí)候顯示的窗口,但是,實(shí)在慚愧,在下還是沒有能夠有一個(gè)實(shí)驗(yàn)結(jié)果,不知道是需要給權(quán)限呢還是需要同時(shí)進(jìn)行其他設(shè)置。同樣,還是很希望有知道的小伙伴能夠在評(píng)論區(qū)向大家分享。WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
點(diǎn)亮屏幕;設(shè)置這個(gè)屬性的窗口,當(dāng)窗口顯示的時(shí)候,如果設(shè)備處于待機(jī)狀態(tài),會(huì)點(diǎn)亮設(shè)備。這個(gè)應(yīng)該在很多鎖屏窗口中用的比較多,比如收到消息點(diǎn)亮屏幕。WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
這個(gè)也不知道怎么去歸納,也是一個(gè)比較有意思的屬性,之前我們說(shuō)到FLAG_NOT_TOUCH_MODAL,在窗口獲得焦點(diǎn)的情況下,當(dāng)觸摸事件是在窗口之外區(qū)域的時(shí)候,窗口不在攔截觸摸事件,而是將事件往下傳遞,而如果再設(shè)置這個(gè)屬性,窗口能在MotionEvent.ACTION_OUTSIDE中收獲窗口之外的點(diǎn)擊事件,遺憾的是不能進(jìn)行屏蔽,也就是說(shuō)事件依然會(huì)向下傳遞。
2. type表示W(wǎng)indow的類型
Window有三種類型,分別為應(yīng)用Window、子Window、系統(tǒng)Window。Window是分層的,每個(gè)Window都有對(duì)應(yīng)的 z-ordered.層級(jí)大的覆蓋在層級(jí)小的上面。
| 說(shuō) 明 | 層級(jí)(z-order)
-----------| ---------------| ------------
應(yīng)用Window | 對(duì)應(yīng)一個(gè)Activity| 1-99
子Window | 常見的Dialog | 1000-1999
系統(tǒng)Window | Toast,系統(tǒng)狀態(tài)欄 | 2000-2999
Window的內(nèi)部機(jī)制
Window是一個(gè)抽象概念,每一個(gè)Window都對(duì)應(yīng)著一個(gè)View和ViewRootImpl,Window 和 View 通過(guò)ViewRootImpl來(lái)建立聯(lián)系。
下圖顯示了Activity的Window和Wms的關(guān)系
Window的添加過(guò)程
Window 的添加需要 WindowManager的addView來(lái)實(shí)現(xiàn),WindowManager是一個(gè)接口,它的實(shí)現(xiàn)是在 WindowManagerImpl類。
public interface WindowManager extends ViewManager{} //WindowManager 是一個(gè)接口
public interface ViewManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
WindowManagerImpl并沒有直接實(shí)現(xiàn) Window的三大操作,而是全部交給WindowManagerGlobal來(lái)處理,WindowManagerGlobal是典型的單例。
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
}
WindowManagerGlobal 中
1. 檢查參數(shù)是否合法
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");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
2. 創(chuàng)建ViewRootImpl并將View添加到列表中
WindowManagerGlobal 內(nèi)部有幾個(gè)列表比較重要:
- 存儲(chǔ)的是所有 window 所對(duì)應(yīng)的 View
private final ArrayList<View> mViews = new ArrayList<View>(); - 所有的 Window 對(duì)應(yīng)的 ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); - 所有 Window 所對(duì)應(yīng)的布局參數(shù)
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
3.通過(guò)ViewRootImpl來(lái)更新界面并完成 Window的添加
這個(gè)步驟由ViewRootImpl的setView方法完成。
// 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.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
setView內(nèi)部會(huì)通過(guò) requestLayout 來(lái)完成異步刷新請(qǐng)求。scheduleTraversals 其實(shí)就是View繪制的入口。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
接著會(huì)通過(guò)WindowSession 最終來(lái)完成 Window 的添加過(guò)程。在下面的代碼中 mWindowSession 類型是一個(gè) IWindowSession,它是一個(gè)Binder對(duì)象,真正的實(shí)現(xiàn)是 Session,也就是說(shuō)Window的添加過(guò)程是一次IPC調(diào)用。
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
}
在Session內(nèi)部會(huì)通過(guò)WindowManagerService來(lái)實(shí)現(xiàn)Window的添加。
final class Session extends IWindowSession.Stub
implements IBinder.DeathRecipient {
...
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
}
如此一來(lái),Window的添加請(qǐng)求就交給了WindowManagerService處理了。在WindowManagerService內(nèi)部會(huì)為每一個(gè)應(yīng)用保留一個(gè)單獨(dú)的Session。
Window的創(chuàng)建過(guò)程
1.Activity的Window創(chuàng)建過(guò)程
從源碼分析Activity的啟動(dòng)過(guò)程
在ActivityThread 的 performLaunchActivity
/**
* 4.創(chuàng)建 ContextImpl 對(duì)象并通過(guò) Activity 的 attach 方法來(lái)完成一些數(shù)據(jù)的初始化
* 將 appContext 對(duì)象 attach 到 Activity 中
*/
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
中,在Activity的 attach方法中,系統(tǒng)會(huì)創(chuàng)建Activity所屬的Window對(duì)象并為其設(shè)置回調(diào)接口。由于Activity實(shí)現(xiàn)了Window的Callback接口,因此當(dāng)Window接受到外界的狀態(tài)改變時(shí)就會(huì)回調(diào)Activity方法。Callback接口中方法很多,常見的有:onAttachedToWindow、onDetachedFromWindow、onWindowFocusChanged等等。Api23的代碼如下:
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
Activity視圖是如何依附在Window上面?
由于Activity視圖由setContentView方法提供,我們只要看setContentView即可。
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
從Activity的setContentView看出,Activity將具體的實(shí)現(xiàn)交給了Window處理,而Window的具體的實(shí)現(xiàn)是PhoneWindow。來(lái)看PhoneWindow的setContentView。
- 如果沒有DecorView,就創(chuàng)建它。
- 將View添加到DecorView的mContentParent中。
- 回調(diào)Activity的 onContentChanged方法,通知Activity視圖改變了。
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
經(jīng)過(guò)了上面的三個(gè)步驟,到這里DecorView已經(jīng)創(chuàng)建并初始化完畢。Activity的布局文件也被成功添加到DecorView的mContentParent中,但這時(shí)候DecorView還沒被WindowManager正式添加到Window中。在ActivityThread的handleResumeActivity中,首先會(huì)調(diào)用Activity的onResume方法,接著會(huì)調(diào)用Activity的makeVisible方法,正是這個(gè)makeVisible方法中,DecorView真正完成了添加和顯示這兩個(gè)過(guò)程。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
參考資料及推薦閱讀
- 本文主要參考了《Android開發(fā)藝術(shù)探索》
- 示例:做一個(gè)炫酷的懸浮迷你音樂盒
- 為什么Dialog不能用Application的Context