【1】Android窗口类型
Android中存在很多窗台类型,类型值从低到高,值越大显示的层越高,本篇使用以下程序为测试代码
private AlertDialog getAlertDialog(String text,int type) {
AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
.setTitle(text)
.setCancelable(true)
.create();
dialog.getWindow().setType(type);
return dialog;
}
总体上分为三大类,以Android-28 API为例,运行环境的android版本也为28,注意,其他android版本设置修改dialog的windowType可能让dialog无法弹出来。
普通窗口
从FIRST_APPLICATION_WINDOW 到 LAST_APPLICATION_WINDOW,这个范围用户可以自行使用,在这个范围内需要AppToken,TYPE_APPLICATION_STARTING这个是在Launcher启动前的过渡窗口类型,并不是Launcher的窗口类型。注意,这里有个问题,如果Dialog的窗口类型低于Activity的窗口类型,那它会显示在Activity下方么?我们使用如下程序测试就知道了。
AlertDialog dialog = getAlertDialog("helloWorld #1",WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW) ;
dialog.show();
AlertDialog dialog2 = getAlertDialog("helloWorld #2",WindowManager.LayoutParams.TYPE_APPLICATION);
dialog2.show();
AlertDialog dialog3 = getAlertDialog("helloWorld #3",WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
dialog3.show();
这里不上图了,结果如下:
- Dialog的展示层级的优先级受TYPE控制,值越大,越显示在上层。
- Dialog的TYPE低于Activity的并不影响其展示在上方,主要原因是Dialog依赖Activity,因此总是展示在parentWindow上方。
- 部分手机上TYPE_APPLICATION_STARTING只能用于Application闪屏之前的Window,且在后续的生命周期中不可再使用
- 部分手机对于低于Activity的Window类型的逻辑存在问题
重点我们知道了AppToken最终影响这类组建的展示,因此如果不使用dialog,我们可以使用如下代码在WindowManager中添加View
private WindowManager mWindowManager;
public void showWindow(final String text, int type) {
View layout = LayoutInflater.from(this).inflate(R.layout.window, null);
TextView vt = (TextView) layout.findViewById(R.id.window_text);
vt.setText(text);
IBinder appToken = getAppToken();
//当然,这里使用appToken==null也是可以的,参考ContextImpl的Context.WINDOW_SERVICE注册逻辑,activity的Window会注入
WindowManager中,在adjustLayoutParamsForSubWindow中能自动复制给params
WindowManager.LayoutParams windowLayoutParams = createWindowLayoutParams(appToken, type);
if(mWindowManager==null){
mWindowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager.addView(layout,windowLayoutParams);
}
private IBinder getAppToken( ) {
IBinder appToken = null;
try {
Field mAppTokenField = Window.class.getDeclaredField("mAppToken");
mAppTokenField.setAccessible(true);
appToken = (IBinder) mAppTokenField.get(TestActivity.this.getWindow());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return appToken;
}
protected final WindowManager.LayoutParams createWindowLayoutParams(IBinder token,int windowType) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
p.gravity = Gravity.CENTER;
p.flags = computeFlags(p.flags);
p.type = windowType;
p.token = token;
p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
p.setTitle("TestWindowManager:" + Integer.toHexString(hashCode()));
return p;
}
private int computeFlags(int flags) {
flags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
return flags;
}
使用方法如下
showWindow("HelloWorld #5",WindowManager.LayoutParams.LAST_APPLICATION_WINDOW);
当然,借道dialog也是可以的
ublic void showWindow2(final String text, int type) {
View layout = LayoutInflater.from(this).inflate(R.layout.window, null);
TextView vt = (TextView) layout.findViewById(R.id.window_text);
vt.setText(text);
Dialog d = new Dialog(this);
d.getWindow().getAttributes().type = type;
d.setContentView(layout);
WindowManager.LayoutParams lp = createWindowLayoutParams(null, type);
//注意,这里没有使用
mWindowManager.addView(d.getWindow().getDecorView(),lp);
}
子窗口
范围从FISRT_SUB_WINDOW到LAST_SUB_WINDOW
这类窗口一般需要使用WindowToken (注意不是AppToken),所谓WindowToken意味着它属于Activity低级别组建,事实上也比Dialog要低,类似PopWindow,这也意味着这类组建没有单独的WindowState监控,此外意味着他们必须在onResume之后弹出,尽管他们的取值大于Dialog的。
当然WindowToken的获取可以参考PopWindow,重点需要关注的WindowToken,同上面要求的AppToken一样,都可以先不设置WindowToken
改造方法如下:
public void showWindow(final String text, int type) {
View layout = LayoutInflater.from(this).inflate(R.layout.window, null);
TextView vt = (TextView) layout.findViewById(R.id.window_text);
vt.setText(text);
WindowManager.LayoutParams windowLayoutParams = createWindowLayoutParams(null, type);
if(mWindowManager==null){
mWindowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager.addView(layout,windowLayoutParams);
}
高级窗口
范围从FIRST_SYSTEM_WINDOW到LAST_SYSTEM_WINDOW
这类组建属于高级组建,重点是可以做app外弹框、应用内常驻弹框,但是由于这类组建在各个版本变动很大,特点是兼容性差、可能需要权限等。
以TYPE_TOAST为例,虽然开发者可以用,但是无法点击,此外添加FLAG_NOT_FOCUSABLE虽然可以常驻,但是部分手机仍然无法点击。
【2】谈谈Dialog的PhoneWindow
- Dialog中创建了PhoneWindow,然而,这个组建并不能为Dialog提供Token的,提供Token的仍然是Activity所在的Window。
- WindowManager并非单例,只为了管理子窗口或组件。Dialog也会为自己创建一个和Activity不同的WindowManager,参考WindowManagerImpl的createLocalWindowManager,这个Manager用来管理Dialog上的子窗口或组件,而非管理Dialog组建,同理,Activity的WindowManager用来管理Activity子窗口或者组件。
- createPresentationWindowManager是为了Presentation管理子窗口或者组建创建,而Presentation依然是Activity的组件。
综上,获取WindowManager时一定需要注意,xxx.getWindow().getWindowManager() 可能存在层级问题,使用时一定要注意。