Window
Window是個抽象類,PhoneWindow是Window唯一的實現類。PhoneWindow像是一個工具箱,封裝了三種工具:DecorView、WindowManager.LayoutParams、WindowManager。其中DecorView和WindowManager.LayoutParams負責窗口的靜態屬性,比如窗口的標題、背景、輸入法模式、屏幕方向等等。WindowManager負責窗口的動態操作,比如窗口的增、刪、改。
Window抽象類對WindowManager.LayoutParams相關的屬性(如:輸入法模式、屏幕方向)都提供了具體的方法。而對DecorView相關的屬性(如:標題、背景),只提供了抽象方法,這些抽象方法由PhoneWindow實現。
public abstract class Window {
// The current window attributes.
private final WindowManager.LayoutParams mWindowAttributes =
new WindowManager.LayoutParams();
public void setLayout(int width, int height) {
final WindowManager.LayoutParams attrs = getAttributes();
attrs.width = width;
attrs.height = height;
dispatchWindowAttributesChanged(attrs);
}
public void setGravity(int gravity) {
final WindowManager.LayoutParams attrs = getAttributes();
attrs.gravity = gravity;
dispatchWindowAttributesChanged(attrs);
}
public void setSoftInputMode(int mode) {
final WindowManager.LayoutParams attrs = getAttributes();
if (mode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
attrs.softInputMode = mode;
mHasSoftInputMode = true;
} else {
mHasSoftInputMode = false;
}
dispatchWindowAttributesChanged(attrs);
}
//下面三個抽象方法將由PhoneWindow實現
public abstract void setTitle(CharSequence title);
public abstract void setContentView(View view, ViewGroup.LayoutParams params);
public abstract void setBackgroundDrawable(Drawable drawable);
}
public class PhoneWindow extends Window implements MenuBuilder.Callback {
@Override
public void setTitle(CharSequence title) {
if (mTitleView != null) {
mTitleView.setText(title);
} else if (mDecorContentParent != null) {
mDecorContentParent.setWindowTitle(title);
}
mTitle = title;
}
@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();
}
}
@Override
public final void setBackgroundDrawable(Drawable drawable) {
if (drawable != mBackgroundDrawable || mBackgroundResource != 0) {
mBackgroundResource = 0;
mBackgroundDrawable = drawable;
if (mDecor != null) {
mDecor.setWindowBackground(drawable);
}
if (mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(drawable != null ? 0 : mBackgroundFallbackResource);
}
}
}
}
Window分類
Window 有三種類型,分別是應用 Window、子 Window 和系統 Window。
應用Window,如:Activity和Dialog。
子Window,如:PopupWindow。
系統窗口,如:Toast,輸入法,狀態欄,導航欄。
Window 是分層的,每個 Window 都有對應的 z-ordered,層級大的會覆蓋在層級小的 Window 上面。在三種 Window 中,應用 Window 層級範圍是 1~99,子 Window 層級範圍是1000~1999,系統 Window 層級範圍是 2000~2999,我們可以用一個表格來直觀的表示:
Window | 層級 |
---|---|
應用 Window | 1~99 |
子Window | 1000~1999 |
系統 Window | 2000~2999 |
這些層級範圍對應着 WindowManager.LayoutParams 的 type 參數,如果想要 Window 位於所有 Window 的最頂層,那麼採用較大的層級即可,很顯然系統 Window 的層級是最大的。當我們採用系統層級時,一般選用TYPE_SYSTEM_ERROR或者TYPE_SYSTEM_OVERLAY,還需要聲明權限。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Window並不存在
Window並不是真實地存在着的,而是以View的形式存在。好吧,我知道這句話聽起來有點矛盾。Window本身就只是一個抽象的概念,而View是Window的表現形式,也就是說View是Window的代表。當我們在談論Window時其實是在談論Window的根View。
要想顯示窗口,就必須調用WindowManager.addView(View view, ViewGroup.LayoutParams params)
。參數view就代表着一個窗口。在Activity和Dialog的顯示過程中都會調用到wm.addView(decor, l);
所以Activity和Dialog的DecorView就代表着各自的窗口。
子窗口
上文中提到Dialog也是應用窗口,不知道大家對此是否有疑惑。一開始的時候,我一直以爲Dialog是子窗口,至少從直觀上來說Dialog應該是個子窗口。
但實際上Dialog的確是一個應用窗口。我們看下Dialog的show()方法就知道了。
public void show() {
……
if (!mCreated) {
dispatchOnCreate(null);
}
onStart();
mDecor = mWindow.getDecorView();
……
WindowManager.LayoutParams l = mWindow.getAttributes();
……
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
public abstract class Window {
// The current window attributes.
private final WindowManager.LayoutParams mWindowAttributes =
new WindowManager.LayoutParams();
/**
* Retrieve the current window attributes associated with this panel.
*
* @return WindowManager.LayoutParams Either the existing window
* attributes object, or a freshly created one if there is none.
*/
public final WindowManager.LayoutParams getAttributes() {
return mWindowAttributes;
}
}
public interface WindowManager extends ViewManager {
public static class LayoutParams extends ViewGroup.LayoutParams
implements Parcelable {
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
}
}
從代碼中可以看到Dialog的WindowManager.LayoutParams是從Window中直接取出來的。而Window.getAttributes()返回的LayoutParams是用無參構造函數創建的,這時LayoutParams.type的值爲TYPE_APPLICATION(TYPE_APPLICATION = 2),位於應用Window的層級範圍內,所以Dialog屬於應用窗口。
那麼應用窗口和子窗口就僅僅只是LayoutParams.type的差別?在UI方面就沒有直觀的差別了嗎?
要搞清楚上面的問題,就得先回顧下控件與子控件的關係。將子控件加入父控件時需要爲子控件設置一個佈局參數,即LayoutParams。這個佈局參數是指子控件相對於父控件的佈局參數。例如:當父控件B爲FrameLayout時,爲子控件C設置佈局參數FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.TOP)。這裏的Gravity.TOP是指父控件的頂部,也就是相對於控件B的位置。所以控件C位於控件B頂部,而不是控件A的頂部。
Window也有佈局參數,即WindowManager.LayoutParams。同理,WindowManager.LayoutParams也應該是窗口相對於父窗口的佈局參數。經過觀察,我得出了這樣一個結論:應用窗口的WindowManager.LayoutParams是相對於屏幕,而子窗口的WindowManager.LayoutParams相對於應用窗口。
下面我們通過一個Demo來驗證一下。下面的代碼中先將WindowTestActivity的窗口高度設置成屏幕的一半,然後再彈出一個應用窗口,且該應用窗口在縱向上偏移屏幕高度的一半。
public class WindowTestActivity extends Activity {
private Button popupWin;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_window_test);//佈局文件中就只有幾個按鈕而已,所以就不貼出來了。
}
boolean isFirst = true;
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && isFirst) {
isFirst = false;
DisplayMetrics dm = getResources().getDisplayMetrics();
WindowManager.LayoutParams wmLp = getWindow().getAttributes();
wmLp.height = dm.heightPixels/2;//將Activity的窗口高度設置爲屏幕的一半
wmLp.gravity = Gravity.TOP;//Activity的窗口位於屏幕頂部
// wmLp.y = 100;//Activity的窗口位置在縱向上偏移屏幕頂部100個像素
getWindow().setAttributes(wmLp);
}
}
/**
* 彈出應用窗口
* @param view
*/
public void onClickPopupApplicationWindow(View view) {
DisplayMetrics dm = getResources().getDisplayMetrics();
//調用無參構造函數,wLayoutParams.type = TYPE_APPLICATION,說明該窗口是應用窗口
WindowManager.LayoutParams wLayoutParams = new WindowManager.LayoutParams();
wLayoutParams.width = dm.widthPixels/2;//窗口的寬度設置成屏幕寬度的一半
wLayoutParams.height = dm.heightPixels/4;//窗口的高度設置成屏幕高度的四分之一
wLayoutParams.gravity = Gravity.CENTER_HORIZONTAL|Gravity.TOP;//窗口水平居中且位於屏幕頂部
wLayoutParams.y = dm.heightPixels/2;//窗口的位置在縱向上偏移屏幕高度的一半
wLayoutParams.dimAmount = 0.4f;//當前窗口後方增加陰影時陰影的透明度
/*
WindowManager.LayoutParams.flags中增加WindowManager.LayoutParams.FLAG_DIM_BEHIND
表示當前窗口後方增加陰影;其餘三個flag是爲了將窗口外圍的點擊事件透傳到後方的窗口
*/
wLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|WindowManager.LayoutParams.FLAG_DIM_BEHIND;
popupWin = new Button(this);
popupWin.setGravity(Gravity.CENTER);
popupWin.setText("這裏是彈出的應用窗口");
popupWin.setBackgroundResource(R.color.colorAccent);
popupWin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getWindowManager().removeView(popupWin);//移除應用窗口
popupWin = null;
}
});
getWindowManager().addView(popupWin, wLayoutParams);//彈出應用窗口
}
}
上圖中,我們可以看到Activity的DecorView的高度只佔了屏幕高度的一半,且彈出的應用窗口緊接在它的正下方。那麼現在將代碼中wmLp.y = 100;
的註釋去掉,令Activity的DecorView在縱向上偏移100個像素。看看效果是怎麼樣的。
從圖中可以看到Activity的DecorView雖然縱向偏移了100個像素,但是彈出的應用窗口的位置卻沒有變,即並沒有隨着Activity的DecorView一起偏移。這說明了這個應用窗口的WindowManager.LayoutParams是相對於屏幕的,而不是相對於Activity的DecorView。
PopupWindow.showAtLocation(View parent, int gravity, int x, int y)是相對於整個窗口來顯示PopupWindow的。至於相對於哪個窗口,就取決於參數parent屬於哪個窗口。在上面的代碼中,再新增如下顯示PopupWindow的代碼。
public class WindowTestActivity extends Activity {
private Button popupWin;
……
/**
* 在Activity的DecorView上彈出PopupWindow
* @param view
*/
public void onClickOnDecorViewPopupSubWindow(View view) {
Button btn = new Button(this);
btn.setGravity(Gravity.CENTER);
btn.setAllCaps(false);
btn.setText("在DecorView上彈出的PopupWindow");
btn.setBackgroundResource(R.color.colorPrimary);
PopupWindow popupWindow = new PopupWindow(btn, ViewGroup.LayoutParams.MATCH_PARENT, 200, true);
popupWindow.setBackgroundDrawable(new BitmapDrawable());
popupWindow.showAtLocation(view, Gravity.BOTTOM, 0, 0);
}
/**
* 在彈出的應用窗口上彈出PopupWindow
* @param view
*/
public void onClickOnPopupAppWinPopupSubWin(View view) {
if (popupWin != null) {
Button btn = new Button(this);
btn.setGravity(Gravity.CENTER);
btn.setAllCaps(false);
btn.setText("在已彈出的應用窗口上彈出的PopupWindow");
btn.setBackgroundResource(R.color.colorPrimary);
PopupWindow popupWindow = new PopupWindow(btn, ViewGroup.LayoutParams.MATCH_PARENT, 200, true);
popupWindow.setBackgroundDrawable(new BitmapDrawable());
popupWindow.showAtLocation(popupWin, Gravity.BOTTOM, 0, 0);//以彈出的應用窗口作爲parent
}
}
}
從代碼和效果圖中可以看出來,只因爲參數parent不同,就使得PopupWindow的相對位置不同。而PopupWindow是一個子窗口,所以子窗口的WindowManager.LayoutParams是相對於應用窗口的。
LayoutParams.token
WindowManager.LayoutParams.token在代碼中的註釋是指標識窗口。
/**
* Identifier for this window. This will usually be filled in for
* you.
*/
public IBinder token = null;
那爲什麼要標識窗口呢。我們先來看看應用窗口和子窗口的token分別是什麼。
不論是Activity還是Dialog的WindowManager.LayoutParams.token都是被賦值了ActivityRecord.appToken。而ActivityRecord.appToken就是Activity的一個標識。所以應用窗口的token是爲了標識該應用窗口屬於哪個Activity。
如果不瞭解Activity與Dialog的WindowManager.LayoutParams.token賦值過程的同學,可以看看下面這兩篇文章
ActivityRecord、ActivityClientRecord、Activity的關係
Android窗口機制(五)最終章:WindowManager.LayoutParams和Token以及其他窗口Dialog,Toast
接着再來看看PopupWindow
public class PopupWindow {
private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
public void showAtLocation(View parent, int gravity, int x, int y) {
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
public void showAtLocation(IBinder token, int gravity, int x, int y) {
if (isShowing() || mContentView == null) {
return;
}
……
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
……
invokePopup(p);
}
private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
// These gravity settings put the view at the top left corner of the
// screen. The view is then positioned to the appropriate location by
// setting the x and y offsets to match the anchor's bottom-left
// corner.
p.gravity = Gravity.START | Gravity.TOP;
p.flags = computeFlags(p.flags);
p.type = mWindowLayoutType;
p.token = token;
……
return p;
}
}
從代碼中可以看到PopupWindow中WindowManager.LayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL。TYPE_APPLICATION_PANEL = 1000,屬於子窗口層級。所以PopupWindow是子窗口。
另外我們還發現PopupWindow中WindowManager.LayoutParams.token的值並不是ActivityRecord.appToken,而是View…getWindowToken(),也就是AttachInfo.mWindowToken。
public class View {
/**
* Retrieve a unique token identifying the window this view is attached to.
* @return Return the window's token for use in
* {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}.
*/
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}
}
一個根View及其下的所有子View都擁有相同的AttachInfo,mWindowToken自然也就相同。而根View又代表着一個窗口,所以mWindowToken可以標識一個窗口。對此不瞭解的同學可以查看這篇文章
Android窗口機制(五)最終章:WindowManager.LayoutParams和Token以及其他窗口Dialog,Toast
由此得出一個結論:子窗口的token是爲了標識哪一個應用窗口是它的父窗口。