読者です 読者をやめる 読者になる 読者になる

techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

AndroidのWindowManagerでViewを表示するときのIMEの制約

Androidのアプリを作っている時にどの画面よりも手前にViewを表示したい時にWindowManagerを使って特定の指定レイヤーに対してaddViewを行うケースがあります。
しかし、このaddView時に発生するIMEの表示の制約があるのでそれをご説明します。

WindowManagerのaddViewで追加したViewでIME表示ができるか

まずAndroidのWindowManagerにaddViewをする処理が次のような処理になります。

        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT);

        windowManager.addView(new View(context), params);

ここで指定されるWindowManager.LayoutParamsの第3引数は追加するViewのレイヤーを指定します。

Android6.0、Android7.0において指定できるレイヤーは次のものになります。

  • TYPE_TOAST
  • TYPE_DREAM
  • TYPE_INPUT_METHOD
  • TYPE_WALLPAPER
  • TYPE_PRIVATE_PRESENTATION
  • TYPE_VOICE_INTERACTION
  • TYPE_ACCESSIBILITY_OVERLAY
  • TYPE_QS_DIALOG
  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_ERROR
  • TYPE_SYSTEM_OVERLAY

他にもありますが他のレイヤーはシステム内部で利用する向けなので割愛します。 このうちAndroid6.0、Android7.0でアプリ開発者が利用できるレイヤーはTYPE_TOASTだけです。 端末開発を行っている場合にはシステムのUIDと同一のUIDを持つアプリの場合には次のレイヤーに追加することができます。

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_ERROR
  • TYPE_SYSTEM_OVERLAY

これらの追加できるレイヤーにaddViewできるかを判断する処理はAndroidのソースコード(AOSP)の「frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java」のcheckAddPermissionにてTYPE_TOASTなどの設定されているかどうかを確認されています。
もし違うレイヤーに追加した場合にはエラーになります。

TYPE_TOASTでIMEを表示する

addViewで表示するView上にEditTextを表示することができます。
しかし、EditTextに対してフォーカスを当てたとしてもIMEが表示されることはありません。
IMEを表示するにはフラグとレイヤーで条件があります。

PhoneWindowManager.java内でIMEを表示することができるかを確認するメソッドcanReceiveInputは下記のように設定されています。

   3812     private boolean canReceiveInput(WindowState win) {
   3813         boolean notFocusable =
   3814                 (win.getAttrs().flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) != 0;
   3815         boolean altFocusableIm =
   3816                 (win.getAttrs().flags & WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0;
   3817         boolean notFocusableForIm = notFocusable ^ altFocusableIm;
   3818         return !notFocusableForIm;
   3819     }

このメソッドがtrueで返却されればIMEが表示できる扱いになります。
よくやる手はLayoutParamsのflagにFLAG_ALT_FOCUSABLE_IMを追加してこのメソッドの返り値をtrueにします。
しかしこれの確認している処理の部分は下記の部分になります。

   4297     @Override
   4298     public void layoutWindowLw(WindowState win, WindowState attached) {
   4299         // We've already done the navigation bar and status bar. If the status bar can receive
   4300         // input, we need to layout it again to accomodate for the IME window.
   4301         if ((win == mStatusBar && !canReceiveInput(win)) || win == mNavigationBar) {
   4302             return;
   4303         }

ここを確認するとステータスバーのレイヤーかナビゲーションバーのレイヤーでないと表示でないと4301のif文でreturnで返却されてしまうようです。
このif文を抜けたあとにIME表示時のViewの位置計算や表示などを行っているためにIMEを表示しているためIMEが表示できません。
また、TYPE_TOASTでしか一般的なアプリではaddViewできないことから一般的なアプリではIMEを表示することができませんので注意しましょう。

対策

対策方法としては1種類しかない状態です。
FacebookのMessengerアプリでチャットヘッド機能がありどの画面上でも表示される機能を持っています。
そこでは丸いチャットヘッド時はWindowManagerでaddViewでViewを追加したものを表示しそれをタップしてメッセージ入力画面を表示した時点でActivityを表示して文字入力を行って実現しているようです。
このやり方の弊害はActivityがオーバーレイしてしまうためもともと動いていたアプリがonPauseに入ってしまうので挙動がおかしくなる可能性があります。
注意しましょう。