Android基础 悬浮窗
1、浮窗为什么会“浮”?
上面讲到Activity的显示过程其实已经揭示了通用界面的显示过程,浮窗的显示过程更为简单:
image.png image.png
做过浮窗的同学应该都明白了,为啥浮窗能脱离Activity而显示,本质上我们是把一个View交给WindowManager来管理了,LayoutParams.type类型决定了这个View显示窗口的类型,不同类型显示的窗口层次(z轴)是不一样的。大方面来讲可以分为应用窗口(APPLICATION_WINDOW)、子窗口(SUB_WINDOW)、系统窗口(SYSTEM_WINDOW)三种类型,应用窗口z轴范围是1~99,子窗口的范围是1001~1999,系统窗口是(2000~2999),所以要实现浮动窗口我们只能在系统窗口范围中实现。
类型 | 常量范围 | 子类 | 值 | 说明 | 例子 |
---|---|---|---|---|---|
APPLICATION_WINDOW | 1~99 | TYPE_BASE_APPLICATION | 1 | ||
TYPE_APPLICATION | 2 | 应用窗口 | 大部分的应用程序窗口 | ||
TYPE_APPLICATION_STARTING | 3 | 应用程序的Activity显示之前由系统显示的窗口 | |||
LAST_APPLICATION_WINDOW | 99 | ||||
SUB_WINDOW | 1000~1999 | FIRST_SUB_WINDOW | 1000 | ||
TYPE_APPLICATION_PANEL | 1000 | 显示在母窗口之上,遮挡其下面的应用窗口。 | |||
TYPE_APPLICATION_MEDIA | 1001 | 显示在母窗口之下,如果应用窗口不挖洞,即不可见。 | SurfaceView,在小窗口显示时设为MEDIA, 全屏显示时设为PANEL | ||
TYPE_APPLICATION_SUB_PANEL | 1002 | ||||
TYPE_APPLICATION_ATTACHED_DIALOG | 1003 | ||||
TYPE_APPLICATION_MEIDA_OVERLAY | 1004 | 用于两个SurfaceView的合成,如果设为MEDIA, 则上面的SurfaceView 挡住下面的SurfaceView | |||
LAST_SUB_WINDOW | 1999 | 最后一个子窗口 | |||
SYSTEM_WINDOW | 2000~2999 | TYPE_STATUS_BAR | 2000 | 顶部的状态栏 | |
TYPE_SEARCH_BAR | 2001 | 搜索窗口,系统中只能有一个搜索窗口 | |||
TYPE_PHONE | 2002 | 电话窗口 | |||
TYPE_SYSTEM_ALERT | 2003 | 警告窗口,在所有其他窗口之上显示 | 电量不足提醒窗口 | ||
TYPE_KEYGUARD | 2004 | 锁屏界面 | |||
TYPE_TOAST | 2005 | 短时的文字提醒小窗口 | |||
TYPE_SYSTEM_OVERLAY | 2006 | 没有焦点的浮动窗口 | |||
TYPE_PRIORITY_PHONE | 2007 | 紧急电话窗口,可以显示在屏保之上 | |||
TYPE_SYSTEM_DIALOG | 2008 | 系统信息弹出窗口 | 比如SIM插上后弹出的运营商信息窗口 | ||
TYPE_KEYGUARD_DIALOG | 2009 | 跟KeyGuard绑定的弹出对话框 | 锁屏时的滑动解锁窗口 | ||
TYPE_SYSTEM_ERROR | 2010 | 系统错误提示窗口 | ANR 窗口 | ||
TYPE_INPUT_METHOD | 2011 | 输入法窗口,会挤占当前应用的空间 | |||
TYPE_INPUT_METHOD_DIALOG | 2012 | 弹出的输入法窗口,不会挤占当前应用窗口空间,在其之上显示 | |||
TYPE_WALLPAPER | 2013 | 墙纸 | |||
TYPE_STATUS_BAR_PANEL | 2014 | 从状态条下拉的窗口 | |||
TYPE_SECURE_SYSTEM_OVERLAY | 2015 | 只有系统用户可以创建的OVERLAY窗口 | |||
TYPE_DRAG | 2016 | 浮动的可拖动窗口 | 360安全卫士的浮动精灵 | ||
TYPE_STATUS_BAR_PANEL | 2017 | ||||
TYPE_POINTER | 2018 | 光标 | |||
TYPE_NAVIGATION_BAR | 2019 | ||||
TYPE_VOLUME_OVERLAY | 2020 | 音量调节窗口 | |||
TYPE_BOOT_PROGRESS | 2021 | 启动进度,在所有窗口之上 | |||
TYPE_HIDDEN_NAV_CONSUMER | 2022 | 隐藏的导航栏 | |||
TYPE_DREAM | 2023 | 屏保动画 | |||
TYPE_NAVIGATION_BAR_PANEL | 2024 | Navigation bar 弹出的窗口 | 比如说应用收集栏 | ||
TYPE_UNIVERSAL_BACKGROUND | 2025 | ||||
TYPE_DISPLAY_OVERLAY | 2026 | 用于模拟第二显示设备 | |||
TYPE_MAGNIFICATION | 2027 | 用于放大局部 | |||
TYPE_RECENTS_OVERLAY | 2028 | 当前应用窗口,多用户情况下只显示在用户节目 |
到这里我们对Android系统的窗口层次有个大致的了解了,Activity是Android应用的四大组件之一,描述的是应用的活动状态和周期,受ActivityManagerService的管理;Window/View是图形窗口的抽象模型,描述的是窗口的绘制信息,受WindowManagerService的管理;Activity聚合Window来和图形窗口产生联系。文章旨在理解一下Android窗体系统的一个雏形
2、越过用户授权使用浮窗
2.1类型为TYPE_PHONE、TYPE_PRIORITY_PHONE、TYPE_SYSTEM_ALERT、TYPE_SYSTEM_ERROR、TYPE_SYSTEM_ERROR这些的窗口都是需要用户授权的.
2.2类型为TYPE_TOAST的不需要
2.2.1在Android 4.4 (api 19)以下TYPE_TOAST是无法获取焦点的,所以4.4以下使用TYPE_PHONE就可以,不需要授权;
image.png
2.2.2输入法的限制
在4.4以上使用TYPE_TOAST还是有些小小的限制,如果浮窗交互中需要输入框,TYPE_TOAST和TYPE_PHONE两种类型窗体对输入法的处理还是有些区别。当我们的浮窗在横屏环境中(浮窗下面的应用是横屏的),输入法默认是全屏的,我们可以通过设置文本属性android:imeOptions=“flagNoExtractUi”来禁止输入法的全屏,同时可以设置窗体属性为adjustResize来适配调整浮窗位置防止输入法盖住输入框。
然而adjustResize这个属性对TYPE_TOAST类型的窗体是无效的,所以如果你的浮窗交互中是需要输入文字的,就不能使用半屏幕输入法的体验了。
image.png为了最大程度的优化体验,我们使用浮窗的流程可以细化为:
image.png
总结
一般来说,根据type值大小关系,可以推出系统窗口在子窗口的上面,子窗口在应用窗口的上面。
在不使用系统悬浮窗的情况下,使用子窗口是最上层的窗口,WindowManager.LayoutParams.LAST_SUB_WINDOW
FloatManager.java
package com.gameassist.plugin.view;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import com.gameassist.plugin.controller.MainController;
import com.gameassist.plugin.utils.Logger;
public class FloatManager {
private final Activity activity;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private WindowManager.LayoutParams layoutParamsFloat = null;
private ViewGroup view;
private Context context;
public ViewGroup getView() {
return view;
}
public FloatManager(Activity activity) {
this.activity = activity;
if (layoutParamsFloat == null) {
layoutParamsFloat = new WindowManager.LayoutParams();
layoutParamsFloat.gravity = Gravity.CENTER;
layoutParamsFloat.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParamsFloat.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParamsFloat.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
layoutParamsFloat.format = PixelFormat.TRANSLUCENT;
layoutParamsFloat.type = WindowManager.LayoutParams.LAST_SUB_WINDOW;
}
if (layoutParams == null) {
layoutParams = new WindowManager.LayoutParams();
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.type = WindowManager.LayoutParams.LAST_SUB_WINDOW;
}
}
public void showFloat(final View view) {
try {
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
if ((event.getAction() == MotionEvent.ACTION_DOWN) && ((x < 0) || (x >= v.getWidth()) || (y < 0) || (y >= v.getHeight()))) {
windowManager.removeViewImmediate(view);
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
windowManager.removeViewImmediate(view);
}
return false;
}
});
windowManager = activity.getWindowManager();
if (windowManager != null) {
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
}
windowManager.addView(view, layoutParamsFloat);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void closeView(View view) {
windowManager.removeViewImmediate(view);
}
@SuppressLint("ClickableViewAccessibility")
public void showVirtualKey(Context context, final Activity activity, String hash, String emuType) {
this.context = context;
view = MainController.getInstance().initView(context, emuType, hash, activity);
try {
if (activity != null) {
Logger.e("view" + view.getParent());
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
}
activity.addContentView(view, layoutParams);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@SuppressLint("ClickableViewAccessibility")
public void showVirtualKeyFloat(Context context, final Activity activity, String hash, String emuType) {
this.context = context;
view = MainController.getInstance().initView(context, emuType, hash, activity);
try {
windowManager = activity.getWindowManager();
if (windowManager != null) {
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
}
Logger.e("view:"+view);
windowManager.addView(view, layoutParams);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void OnActivityPause(Activity activity) {
try {
if (null != windowManager) {
Logger.e("OnActivityPause:windowManager");
windowManager.removeViewImmediate(view);
}
} catch (Exception e) {
Logger.e(e.getMessage());
}
}
public void setVisibleChild(String tag, int visible) {
for (int i = 0; i < view.getChildCount(); i++) {
View viewi = view.getChildAt(i);
Object tmp = viewi.getTag();
if (tmp != null) {
if (tmp.toString().startsWith(tag)) {
viewi.setVisibility(visible);
}
}
}
}
}