认识一下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是为了标识哪一个应用窗口是它的父窗口

发布了37 篇原创文章 · 获赞 8 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/jiejingguo/article/details/102596259