Easy Custom Keyboard with InputConnection

input-connection-0

As Android developers, we often find ourselves in need of creating custom components. One common case is creating a custom keyboard.

The first solution most devs probably think of involves creating a custom XML keyboard, gluing a bunch of on click listeners to it, and probably having a special custom logic on setting that same value to an EditText or some other view. Sounds like a lot of work.

Fortunately, there is an interface called InputConnection, which can really make our lives easier and save us a lot of time. It was added in API level 3, and appears to have remained unnoticed by many developers.

What is the InputConnection interface?

As per documentation: “The InputConnection interface is the communication channel from an InputMethod back to the application that is receiving its input. It is used to perform such things as reading text around the cursor, committing text to the text box, and sending raw key events to the application.“

Before further explanation, let’s take a look at two terms which might be useful in creating a bigger picture here:

  • Input Method Engine – can be anything that can produce any form of text represented information. In our case, that’s a keyboard.
  • Editor – would represent any kind of component which is responsible for displaying the input produced from an Input Method Engine. In most cases, that would be a simple EditText.

So in layman’s terms, InputConnection acts as a glue between the Input Method Engine and an Editor which is responsible for displaying the input.

Now, enough of the chit chat and lets hop onto some examples.

Custom keyboard implementation

Let’s assume we already have a custom keyboard XML layout prepared and now we’re deciding on which actions to take for connecting the keys with a certain EditText on the screen(or any class which subclasses a TextView).

It would look approximately like this:

Custom keyboard implementation

To be able to use the InputConnection you need to have an EditText for which you can return/create its InputConnection:

	   val inputConnection = currentlyFocusedEditText.onCreateInputConnection(EditorInfo())

InputConnection needs to have certain information about the EditText it’s connected to. So EditorInfo()’s job here would be to provide it. Most important type of information that EditorInfo() should provide are current cursor position and type of text content. There are plenty of other choices which might be handy, so feel free to check them out.

Be careful if working with custom views and EditorInfo() as there could potentially be many things going wrong, so please do check official docs in detail.

Now that we have inputConnection connected to its currentlyFocusedEditText, we can do some actions with it:

	 val inputConnection = currentlyFocusedEditText.onCreateInputConnection(EditorInfo())

 private val keyPadListener = OnClickListener { view ->
         val value = (view as? TextView)?.text
         inputConnection?.commitText(value, 1)
 }

As we’re dealing with a simple numpad, this would be the point in which we create a click listener to be able to react to certain keystroke actions. The main objective here would be to pass selected number values to the currentlyFocusedEditText.

InputConnection gives us a simple way to achieve this with commitText(String, int) method:

  • commitText(String, 1) – textValue(String) will remove the contents of the currently composed text and replace it with the first parameter String(textValue).
  • commitText(textValue, Int) – 1(Int) sets the new cursor position. If you pass an Integer value that is <= 0 the cursor will be placed BEFORE the committed text. However, if you pass a 1 like in our case which is > 0, then the cursor will be placed AFTER the committed text.

To make our implementation more complete we still need to take care of delete action. So, I suggest creating an InputConnnection.delete() extension function that would look approximately like this:

	private fun InputConnection.delete() {
        If (getSelectedText(0).isNullOrBlank()) {
            deleteSurroundingText(1, 0)
        } 
        commitText("", 1)
    }
}

Another cool feature of InputConnection is to fetch the selected text with a specific return option. With getSelectedText(Int), the Int parameter serves as a flag to specify the return format. For example, the flag value of 0 would return the text as a regular String and GET_TEXT_WITH_STYLES would return a SpannableString with all the related styles.

If you’re wondering about isNullOrBlank() check, then commitText(String,Int) documentation should explain it: “If there is no composing text when this method is called, the new text is inserted at the cursor position, removing text inside the selection if any.”

This is where deleteSurroundingText(Int, Int) method comes into picture:

  • The first parameter specifies the number of characters to be deleted before the current cursor position
  • The second one specifies the number of characters to be deleted after the cursor

Good thing with this one is there’s no IndexOutOfBoundsException thrown here, so if you pass in an int value that is too large, the method will take care of it, and wipe out all characters in the specified range.

With that out of the way, let’s improve our keyPadListener implementation:

	val inputConnection = currentlyFocusedEditText.onCreateInputConnection(EditorInfo()) 

private val keyPadListener = OnClickListener { view ->
 when (view.id) {
  R.id.deleteKey-> inputConnection?.delete()
  else -> {
   val value = (view as? TextView)?.text
   inputConnection?.commitText(value, 1)
  }
 }
}

private fun InputConnection.delete() {
 if (getSelectedText(0).isNullOrBlank()) {
  deleteSurroundingText(1, 0)
 } else {
  commitText("", 1)
 }
}

This pretty much covers all the basic work we must do here.

Of course, I’m leaving all the nitty gritty details like setting the keyPadListener on your desired view or hiding system keyboard and all the use case-specific stuff to you.

Don’t overlook these either

Remember, if you were to implement your own editor view, then you would need to subclass BaseInputConnection class to be able to use it.

Also, keep in mind that the InputConnection interface has around 26 methods you can use, and we covered only the most basic ones here.

A couple of other methods worth highlighting:

  • setSelection(int, int) which returns a boolean if the start selection index and end selection index passed in values are satisfied (the text has been successfully selected)
  • commitCompletion(CompletionInfo()) which returns a boolean which gives you another form of confirmation that your commit did in fact succeed and was displayed on your editor.

Further reading suggestions

There is much to be shown or talked about regarding InputConnection, so I advise you to check out the full documentation for this powerful interface.