Android悬浮框无法弹出输入法

最近要研究悬浮窗方面的东西,遇到一个问题,我的悬浮窗里面有一个输入框,但是不弹出输入法,后来找到一个方法:

在WindowManager的实例获取方式不对,之前是这样获取的:

mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);

改这样:

mWindowManager = (WindowManager) 
mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);

一个是通过当前activity的上下文环境去获取窗口服务,一个是通过application去获取窗口服务

发现还是不行,后来我花了很长时间用谷歌翻译了WindowManager的源码英文注释才发现,原来我之前是用的:

bigWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

LayoutParams.FLAG_NOT_TOUCH_MODAL的意思是:

/**
         窗口标志:即使该窗口是可对焦的(其#FLAG_NOT_FOCUSABLE未设置),
         允许窗口外的任何指针事件发送到其后面的窗口。
         否则它将消耗所有指针事件本身,而不管它们是否在窗口内。
          */
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;

即不会阻挡后面的点击事件

LayoutParams.FLAG_NOT_FOCUSABLE的意思是:

/**
         窗口标志:此窗口不会获得关键输入焦点,因此用户无法向其发送键或其他按钮事件。
         那些会改变它背后的任何可关注的窗口。
         此标志还将启用#FLAG_NOT_TOUCH_MODAL是否显式设置。
         *
         *
         设置此标志也意味着该窗口不需要与软输入法进行交互,
         因此它将被Z-排序并且与任何活动输入法无关(通常这意味着它在输入法之上得到Z-排序,
         所以它可以使用全屏的内容,如果需要的话可以覆盖输入法。
         可以使用{@link #FLAG_ALT_FOCUSABLE_IM}来修改这个行为。
         * */
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;

该窗口不需要与软输入法进行交互,说明是不会弹出输入框的,把这个去掉,就能正常显示出输入法了,不过去掉了也会造成另外一个问题,那就是系统的返回键用不了!

要想在WindowManager中直接添加EditText并且能够输入文字,而且还不影响其他操作的,需要保证以下两点:

type 为 LayoutParams.TYPE_SYSTEM_ALERT 而不是 LayoutParams.TYPE_SYSTEM_ERROR

防止没有三个按键的问题
flag中不要有 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

防止无法弹出输入框的问题
flag中要有 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

防止下方app无法获取焦点和事件的问题

看来花费了这么多时间翻译了一下源码的注释还是很有好处的,起码知道了用这个属性是为什么,也说明要想学得好还是得多看源码,翻译一下注释。

无需权限显示悬浮窗

悬浮窗原理

做过悬浮窗功能的人都知道, 要想显示悬浮窗, 要有一个服务运行在后台, 通过getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 然后向其中addView, addView第二个参数是一个WindowManager.LayoutParams, WindowManager.LayoutParams中有一个成员type, 有各种值, 一般设置成TYPE_PHONE就可以悬浮在很多view的上方了, 但是调用这个方法需要申请android.permission.SYSTEM_ALERT_WINDOW权限, 在很多机型上, 这个权限的名字叫悬浮窗, 比如小米手机上默认是禁用这个权限的, 有些恶意app会用这个权限弹广告, 而且很难追查是哪个应用弹的. 如果这个权限被禁用, 那么结果就是悬浮窗无法展示, 比如有道词典的复制查词功能, 在小米手机上经常没用, 其实是用户没有授权, 而且应用也没有引导用户给它打开授权。

使用TYPE_TOAST可以显示悬浮框

使用 type 值为 WindowManager.LayoutParams.TYPE_PHONE 和 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT 需要申请 android.permission.SYSTEM_ALERT_WINDOW 权限,否则无法显示,报错:

E/AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRoot$W@b64b5458 -- permission denied for this window type

但是,将type设置成TYPE_TOAST果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW权限就能显示一个悬浮窗.

原因

在Android源码中有这么一段:

public int checkAddPermission(WindowManager.LayoutParams attrs) {
    int type = attrs.type;

    if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
            || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        return WindowManagerImpl.ADD_OKAY;
    }
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            break;
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
            // The window manager will check these.
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    if (permission != null) {
        if (mContext.checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) {
            return WindowManagerImpl.ADD_PERMISSION_DENIED;
        }
    }
    return WindowManagerImpl.ADD_OKAY;
}

这个方法是往系统的WindowManager里addView的时候做权限检查用的, 那个type就是我们在构造WindowManager.LayoutParams时赋值的type, 可以看到, 除了TYPE_TOAST, 其他都是要权限的, 而且非常喜感的是, 代码中的注释还说他们现在对这种type毫无限制, 应该引入标记来限制开发者.

兼容性

  1. type 值为 WindowManager.LayoutParams.TYPE_TOAST 显示的 System overlay view 不需要权限,即可在任何平台显示。
  2. 但仅在 API level >= 19 时可以达到目的。API level 19 以下因无法接收无法接收触摸(点击)和按键事件,故无法达到目的。
  3. 对于 API level < 19 的机器(MIUI除外),想要达到目的,需要:

要有 android.permission.SYSTEM_ALERT_WINDOW 权限
将 type 设置为 WindowManager.LayoutParams.TYPE_PHONE 或者 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
MIUI V5

在给悬浮窗权限之后,表现同 3 。但是,不给悬浮窗权限时,应用在前台时,却可以显示。这点非常不一样。

为什么采用 TYPE_TOAST

API level 19 之后,TYPE_SYSTEM_ALERT 就可以达到 TYPE_SYSTEM_ALERT 效果(即可以达到目的)。

但在 API level 23 (Android 6.0) 上,悬浮窗权限也单独弄出来了,需要到单独开启,这个处理和现在 Smartisan 还有小米类似。

故采用 TYPE_TOAST 能让用不开启悬浮窗权限的情况下,也能显示。为的就是尽量少请求权限。

为什么 API level 19 之后 TYPE_TOAST 可以接受事件

PhoneWindowManager.adjustWindowParamsLw(),API level 19 后做了调整。

当我们使用 TYPE_TOAST, Android 会偷偷给我们加上 FLAG_NOT_FOCUSABLE 和 FLAG_NOT_TOUCHABLE , 4.0.1 开始, 会额外再去掉 FLAG_WATCH_OUTSIDE_TOUCH。 这样真的是什么事件都没了。

而4.4开始, TYPE_TOAST被移除了。所以从 4.4 开始, 使用 TYPE_TOAST 的同时还可以接收触摸事件和按键事件了, 而4.4以前只能显示出来, 不能交互。

最后

TYPE_TOAST一直都可以显示, 但是用TYPE_TOAST显示出来的在2.3上无法接收点击事件, 因此还是无法随意使用.
下面是我之前研究后台线程显示对话框的时候记得笔记, 大家可以看看我们项目中有需求需要在后台任务中显示Dialog, 项目最初的做法是用Activity模拟Dialog, 一个Activity已经承载了近20种Dialog, 代码混乱至极. 后来我发现Dialog可以通过改变Window Type实现不依赖Activity显示, 然后就很兴奋的要在使用这种方式来作为新的实现方式.
最初WindowType是WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 可是这是悬浮窗了, MIUI会默认禁止(真他妈操蛋,也没有任何提示)最终放弃. 后来试着换成了WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI也不禁止了, 哪里都能显示, 这下开心了. 可是后来又发现在2.3上不能接收点击事件, 也就是说Dialog上的按钮不能点击, 这他妈就很操蛋了, 又放弃了. 又试了试其他的Type都不能满足需求, 结果如下:TYPE_SEARCH_BAR: 未知
TYPE_ACCESSIBILITY_OVERLAY: 拒绝使用
TYPE_APPLICATION: 只能配合Activity在当前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在当前APP使用
TYPE_APPLICATION_MEDIA: 无法使用(什么也不显示)
TYPE_APPLICATION_PANEL: 只能配合Activity在当前APP使用(PopupWindow默认就是这个Type)
TYPE_APPLICATION_STARTING: 无法使用(什么也不显示)
TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在当前APP使用TYPE_BASE_APPLICATION: 无法使用(什么也不显示)
TYPE_CHANGED: 只能配合Activity在当前APP使用
TYPE_INPUT_METHOD: 无法使用(直接崩溃)
TYPE_INPUT_METHOD_DIALOG: 无法使用(直接崩溃)
TYPE_KEYGUARD_DIALOG: 拒绝使用
TYPE_PHONE: 属于悬浮窗(并且给一个Activity的话按下HOME键会出现看不到桌面上的图标异常情况)
TYPE_TOAST: 不属于悬浮窗, 但有悬浮窗的功能, 缺点是在Android2.3上无法接收点击事件
TYPE_SYSTEM_ALERT: 属于悬浮窗, 但是会被禁止

想要像银联一样,在某Activity做到手机无法截屏,甚至是adb也拿不到,那么可以在Activity中加入:

getWindow().addFlags(WindowManager.LayoutParams. FLAG_SECURE);

源码:

/**
         窗口标志:将窗口的内容视为安全的,防止窗口显示在屏幕截图中或在非安全显示器上查看。

         有关安全表面和安全显示的更多详细信息,请参阅android.view.Display#FLAG_SECURE。
         */
        public static final int FLAG_SECURE             = 0x00002000;