Showing the Android Keyboard Reliably

Making the Android keyboard work better

Reddit
LinkedIn

When we develop our Android applications, the keyboard is something we rarely think about. It’s a service provided by the platform and it’s crucial it’s that way. It conveys so much functionality and covers so many cases that it would be nearly impossible to provide an international friendly keyboard that works as intended in all cases.

We limit ourselves to set the keyboard options for each one of our fields and Android will do the rest.

Showing the keyboard

There’s nevertheless one case where we might need to interact with the keyboard. And that is showing it when we navigate to a screen with only one field and we know the user will need to input data. That way we save the user a click on the field and the navigation is simplified. That’s very important if that use case is repeated multiple times or we are in a time-sensitive context, like serving our customers.

Android Keyboard

Our approach so far

The Android platform offers some API to run this interaction. The InputMethodManager. It offers a showSoftInput method which we can call to show the keyboard. But it requires us to pass a View. And it doesn’t show in all cases.

At Square we had an extension method to simplify this, but it had its limitations.

/**
* Show the soft keyboard. On phones with a hard keyboard has the unfortunate side effect
* of leaving the keyboard showing until we or the user dismiss it, even when going
* to another application.
*/
fun View.showSoftKeyboard(force: Boolean = false) {
 val inputMethodManager =
    context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
 if (force) {
   inputMethodManager.toggleSoftInput(
       InputMethodManager.SHOW_FORCED,
       InputMethodManager.HIDE_IMPLICIT_ONLY
   )
 }

 val hasHardKeyboard = context.resources.configuration.hasHardKeyboard()
 val hardKeyboardInUse = context.resources.configuration.isHardKeyboardInUse()
 if (hasHardKeyboard && !hardKeyboardInUse) {
   inputMethodManager.showSoftInput(this, InputMethodManager.SHOW_FORCED)
 } else if (!hasHardKeyboard) {
   inputMethodManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
 }
}

As you can see the method does two things:

  • takes care of when the device has a hardware keyboard – in use or not. You might find yourself in this situation when running the standard Android emulator: it counts your computer keyboard as a hardware keyboard.
  • accepts a “forced” parameter.

Why is there a forced parameter?

The InputMethodManager asks for a View because the keyboard is shown for the particular window containing the view, and it won’t show the keyboard if that window does not have focus already.

When we request focus for a View, that View gets focus only when the containing window has focus as well. Normally that’s not a problem. But it might if the window is just in the process of getting focus. For those cases we use SHOW_FORCED. But our QA team found a nasty side effect: the keyboard doesn’t go away if the window loses focus or is backgrounded. It only goes away if the user clicks the back button or we hide it programmatically.

Android Keyboard GIF

Therefore the way in which Android handles windows, focus, and showing the keyboard present two suboptimal options:

  • Reliably showing the keyboard by forcing it, but having a zombie keyboard when the app is backgrounded (as in the animation).
  • Not showing the keyboard reliably, or using some kind of delay to ensure the window is focused.

A better solution

We found a better solution that might be handy as well.

  • We use the SHOW_IMPLICIT option. That’s the right one for the keyboard to behave properly.
  • We need the window to be focused already.
  • We cannot just post a runnable that shows the keyboard in the hope that the next UI loop will already have focus: focus might be obtained after several UI loops.
  • We don’t want to use a timer with an arbitrary time.
  • Therefore: we need to be notified when the window is ready.

There’s one more detail: InputMethodManager decides if the window is ready to show the keyboard. That happens just after the onWindowFocusChanged event handler is called, but before the next iteration of the UI loop. That means that we cannot just call showSoftInput from the onWindowChanged, but we need to post that code for the next UI loop.

You can verify it for yourself. We already confirmed it on API 21, API 27 and API 29:

  • InputMethodManager.showSoftInput checks that mServedView is not null.
  • mServedView is set to a non null value only in InputMethodManager.checkFocusNoStartInput.
  • InputMethodManager.checkFocusNoStartInput gets called from:
  • ViewRootImpl.handleMessage case MSG_WINDOW_FOCUS_CHANGED
  • Which is called just after mView.dispatchWindowFocusChanged is invoked.

Therefore we know how to correctly show the keyboard in any situation:

  • When focusAndShowKeyboard is requested, check if the window is focused.
  • If not, wait until onWindowFocus is called.
  • When it’s called post a runnable that will run on the next UI loop.
  • That runnable will call showSoftInput knowing that it will be honored.

As we need to add code to onWindowChanged we used a custom EditText subclass.

class FixedKeyboardEditText(context: Context, attributeSet: AttributeSet?) : EditText(context, attributeSet) {
 private var showKeyboardDelayed = false
 /**
  * Will request focus and try to show the keyboard.
  * It has into account if the containing window has focus or not yet.
  * And delays the call to show keyboard until it's gained.
  */
 fun focusAndShowKeyboard() {
   requestFocus()
   showKeyboardDelayed = true
   maybeShowKeyboard()
 }

 @Override
 override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
   super.onWindowFocusChanged(hasWindowFocus);
   maybeShowKeyboard()
 }

 private fun maybeShowKeyboard() {
   if (hasWindowFocus() && showKeyboardDelayed) {
     if (isFocused) {
       post {
         val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
         imm.showSoftInput(this@FixedKeyboardEdit, InputMethodManager.SHOW_IMPLICIT)
       }
     }
     showKeyboardDelayed = false
   }
 }
}

An even better solution

If you don’t have a custom EditText class to add the fix to –or alternatively, if you want to add this feature to several types of views – it would be nice to use a Kotlin extension method instead. You need to be notified when the containing windows receives focus, and views cannot tell you that, but we can resort to the ViewTreeObserver:

fun View.focusAndShowKeyboard() {
   /**
    * This is to be called when the window already has focus.
    */
   fun View.showTheKeyboardNow() {
       if (isFocused) {
           post {
               // We still post the call, just in case we are being notified of the windows focus
               // but InputMethodManager didn't get properly setup yet.
               val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
               imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
           }
       }
   }

   requestFocus()
   if (hasWindowFocus()) {
       // No need to wait for the window to get focus.
       showTheKeyboardNow()
   } else {
       // We need to wait until the window gets focus.
       viewTreeObserver.addOnWindowFocusChangeListener(
           object : ViewTreeObserver.OnWindowFocusChangeListener {
               override fun onWindowFocusChanged(hasFocus: Boolean) {
                   // This notification will arrive just before the InputMethodManager gets set up.
                   if (hasFocus) {
                       this@focusAndShowKeyboard.showTheKeyboardNow()
                       // It’s very important to remove this listener once we are done.
                       viewTreeObserver.removeOnWindowFocusChangeListener(this)
                   }
               }
           })
   }
}