認識一下Android中的Window

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是爲了標識哪一個應用窗口是它的父窗口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章