Android
Kotlin

AndroidのSearchViewで表示されるキーボードを隠す対応を検討した結果こうなった

初めに

本記事はDroidKaigi2018でIssueを担当して学んだことの覚え書きです。
検索ボタンを押した後にタブやナビゲーションバーで遷移するとキーボードが表示されたままになってしまうので、適切なタイミングでキーボードを消すというIssueを担当しました。
今回始めてこの様な制御をしたので、突っ込みどころありましたら突っ込んで頂けると助かります。

概要

担当したIssueは以下の3件です。

  1. 検索を押した後にタブやBottomNavigationBarで遷移するとキーボードが表示されたままになる
  2. 検索を押した後にドロワーを出すとキーボードが表示されたままになる
  3. 検索を入力した後にフォーカスが外れると検索結果が消えてしまう

3は2を直した後にバグを入れてしまって、それを直しただけです…。

結果

まずは修正した最終結果です。

SearchFragment.kt
searchView.setOnQueryTextFocusChangeListener { view, hasFocus ->
    if (!hasFocus) {
        if (TextUtils.isEmpty(searchView.query)) {
            searchView.isIconified = true
        } else {
            val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as
                    InputMethodManager
            imm.hideSoftInputFromWindow(view.windowToken, 0)
        }
    }
}
DrawerMenu.kt
object : ActionBarDrawerToggle(
        activity,
        drawerLayout,
        toolbar,
        R.string.nav_content_description_drawer_open,
        R.string.nav_content_description_drawer_close
) {
    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        super.onDrawerSlide(drawerView, slideOffset)
        if (activity.currentFocus is SearchView.SearchAutoComplete) {
            drawerView.requestFocus()
        }
    }
}

この様な結果となりました。

解説

1.の対応

1.のIssue(タブやナビゲーションバーで遷移してもキーボードが出たまま)の対応は以下で出来ました。

SearchFragment.kt
searchView.setOnQueryTextFocusChangeListener { view, hasFocus ->
    if (!hasFocus) {
        val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as
                InputMethodManager
        imm.hideSoftInputFromWindow(view.windowToken, 0)
    }
}

適切なListenerを探すのが大変で、色々試した結果setOnQueryTextFocusChangeListenerがSearchViewのクエリのフォーカスの状態が変わった時に動作するので、hasFocusがFalseの時(ここはアドバイス頂きました:bow:)にキーボードを隠す様にしました。
これでタブやナビゲーションバーの遷移時にフォーカスが外れた時は自動でキーボードが隠れるようになりました。

gif画像付きのPRはこちら

2.の対応

1.の対応を終えて、フォーカスが外れた時にキーボードが消えるのだからもう大丈夫だろう…と思っていたらそうではありませんでした:innocent:
ドロワーを出す時はフォーカスがクエリから外れません。
そうなんだー…と思うと同時にテストから漏れていた点も反省点でした。
という訳でせっかくなら最後まで、という事でこちらも担当させて頂きました。
以下の様に直しました。

SearchFragment.kt
searchView.setOnQueryTextFocusChangeListener { view, hasFocus ->
    if (!hasFocus) {
        searchView.isIconified = true
    }
}
DrawerMenu.kt
object : ActionBarDrawerToggle(
        activity,
        drawerLayout,
        toolbar,
        R.string.nav_content_description_drawer_open,
        R.string.nav_content_description_drawer_close
) {
    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        super.onDrawerSlide(drawerView, slideOffset)
        if (activity.currentFocus is SearchView.SearchAutoComplete) {
            drawerView.requestFocus()
        }
    }
}

こちらは修正量は少ないのですが、結構調査に時間がかかりました。
やりたいことはドロワーに連動してキーボードを隠すようにしたかったのですが、連動させるタイミングによって動作が色々と変わるのでどれがベストか?というところも悩みました。

onDrawerOpenedで隠す

onDrawerOpenedを使うとドロワーが開ききった後にキーボードが隠れるという動作になりました。
既存のアプリで同じような動作のアプリを調べてみたところ、Discordが同じ動作でした。
これはドロワーが開ききった後に一度呼ばれるだけなので、呼ばれたタイミングでキーボードを隠せば目的は達成出来そうでした。
ただし、それだけだとどんな時でも呼ばれてしまうので、SearchView.SearchAutoCompleteにフォーカスが当たっている時だけとしました。

onDrawerSlideで隠す

どうしてもドロワーが出て来るタイミングでキーボードを隠したいという思いがありました。
既存のアプリではSlackがこの動作を実現していました。
他に使えそうなメソッドはないかと調べていたところ、onDrawerSlideが使えそうでした。
早速こちらに変更してみたところ、ドロワーが出て来る瞬間に隠れる様になりましたが、こちらのメソッドは一度だけでなく、完全にドロワーが出てくるまで何度も呼ばれるという挙動でした。
流石にキーボードを隠す処理を何度も呼ぶのは影響があるだろうと思い、一度だけ呼ばれる方法は無いだろうかと考えたところ、フォーカスをドロワーが取ることでクエリのロストフォーカスを発生させることでした。
これでドロワーが開く瞬間にキーボードを隠す事が出来るようになりました。

まだこだわりたい

キーボードは隠せたのですが、ドロワーを閉じた後にクエリにフォーカスが戻る動作となるのですが、そうではなくてSearchボタンを閉じた状態に出来ないかと考えました。
これも色々と調べたところ、isIconified = trueをセットすればよい事が分かりました。
じゃあキーボードを隠すんじゃなくて最初からこれを使えばいいのではと思い、キーボードを隠す処理をこちらに変更して色々と遷移させたところ、問題なさそうだったのでこれでPRを出しました。
大事なテストが漏れていたことも知らずに…。

gif画像付きのPRはこちら

3.の対応

2の対応を終えてしばらくした後に検索がうまく動かないよ的なIssueが上がってきて、あ、多分さっきの修正だな…と気付き直ぐ様手を上げました。
原因はロストフォーカスした時に何でもかんでもクエリを閉じてしまっていたので、普通に検索した後にフォーカスをロストしてもクエリが閉じてしまって検索結果が消えてしまっていました。
検索周りを修正したのに検索をテストし忘れるとは…orz
とにかくきちんと直す必要があったので再度ソースとにらめっこし、閉じてもいい状態と駄目な状態を切り分けようと検討したところ、クエリに文字が入力されていなかったら閉じてもいい、そうでなかったら駄目という単純な条件で良さそうだと考えました。
修正した結果は以下の通りです。

SearchFragment.kt
searchView.setOnQueryTextFocusChangeListener { view, hasFocus ->
    if (!hasFocus) {
        if (TextUtils.isEmpty(searchView.query)) {
            searchView.isIconified = true
        } else {
            val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as
                    InputMethodManager
            imm.hideSoftInputFromWindow(view.windowToken, 0)
        }
    }
}

これでようやく検索もちゃんと出来る、遷移した時もドロワーが出てきた時もキーボードが消えるという動作が実現出来ました。

これ以上バグが無い事を祈っています…。

gif画像付きのPRはこちら

Issueを担当してみて

検討つかないけど多分出来るだろうというIssueを担当してみた結果、色々と調べるよいきっかけになりました。
皆さんすごい勢いでIssueをこなしていく中、アレコレ試行錯誤しながら一週間くらいかけてマイペースにPRを出していました。
(プレッシャーは感じていましたが…)
PRを出した後もコメントが丁寧ですし、mhidakaさんは優しいコメントを残してくれるし(バグ出した時も気にしないでとコメントがついてました)、勉強になるしでよい事ばかりでした。
Android勉強中の身ですが、挑戦してみてよかったです。
もうすぐDroidKaigi本番ですが、出来そうな事がありましたら何かコントリビュートしてみるのもよいのではないでしょうか。
もしくは来年チャンスがあれば何かしらコントリビュートしてみると、きっとよい経験になると思います。