Adventures in the Land of EditText

Feburary, 2nd 2020

Together with my friend Pol I recently worked on Napkin. The Android app combines both a notepad and a calculator into a single experience. During development we stumbled upon several small challenges. This post tries to summarize our experience and learnings with regard to EditText.

Napkin mainly consists of a single screen which acts as a text editor. To make the app more visually appealing we decided to highlight keywords, titles and variable declarations.

Napkin App Screenshot
main screen of the app

Basic Setup

The basic setup is quite straightforward, after adding an EditText to our layout we were ready to rock. In order to support multi-line text the inputType attribute is set accordingly.

Here’s how the layout looks like:

<EditText
          android:id="@+id/input"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:inputType="textMultiLine|textNoSuggestions"
          tools:text="Hello World" />

Learning 1: Configuring the keyboard is hard

The inputType attributes also controls the look of the keyboard. For our use case we want the keyboard to show the digit bar on top, but unfortunately there’s no flag for that. Some people on the web recommended using textVisiblePassword but that actually has other side affects. E.g. deleting multiple characters by holding the delete key stops working. On top of that we found out that every keyboard app behaves differently, for example GBoard seemed to completely ignore textNoSuggestions.

After trying out all possible combinations, we settled on textMultiLine|textNoSuggestions and accepted the limitations given by the system.

Listening for changes

We want our text editor to automatically perform formatting as you type, so we need to react on every keystroke. This is done by attaching a TextWatcher listener to our EditText. The android-ktx library even offers a handy Kotlin extension function to register for changes by providing a lambda function:

inputView.addTextChangedListener { text: Editable? ->
	TODO("implement")
}

Styling Text

The EditText class extends the TextView class and uses the same styling mechanism: Spans. A Span is an object which is used by TextView when rendering or layouting text.

The easiest way to create text with spans is by using the APIs provided by SpannableStringBuilder. There are many subclasses of Span provided by the system. Apart from the official documentation there are a lot of great blog posts about this topic. Check them out if you want to learn more!

For our use case we are simply using a ForegroundColorSpan which applies the desired text color to our headings, variables and keywords.

Learning 2: Don’t call setText() after every input change

Our initial approach was to process the input text in the background, build a formatted SpannableString and use EditText.setText() to set the newly formatted text. This is not ideal because of several reasons:

Luckily Android provides a better way to deal with this: Specifically EditText: getText() returns an Editable object, which itself provides APIs to add and remove spans of the current input text. This allows us to apply the spans without calling EditText.setText(). The relevant API looks like this:

interface Editable : CharSequence, GetChars, Spannable, Appendable {
	fun clearSpans()
	fun setSpan(what: Any!, start: Int, end: Int, flags: Int)
    ...
}

Happy that we’ve found a solution, we switched to using the methods above. Our approach now looked like this:

Once our background calculation is ready we:

Learning 3: Don’t call clearSpans() when using EditText

With this new approach we noticed a new strange behavior: After setting the new spans, the EditText stopped showing a text cursor and didn’t process input anymore.

As mentioned in the beginning spans are used to style text. It turns out that text selection and the text cursor are spans too. And as you might have guessed by now clearSpans() does a really great job at deleting ALL spans :) So calling clearSpans() on an EditText probably is never a good idea as it makes your EditText completely useless.

Luckily Editable also provides methods to retrieve all spans by class type and also a way to remove individual spans. Here’s our extension function which combines all those methods:

inline fun <reified T> Editable.removeSpans() {
    val allSpans = getSpans(0, length, T::class.java)
    for (span in allSpans) {
        removeSpan(span)
    }
}

Happy to have our text cursor back, we continued our journey and quickly stumbled across another issue…

Learning 4: Modifying spans fires TextWatcher

We quickly discovered that our span changes actually fired our TextWatcher listener which in turn caused an endless loop. So the listener should ignore any changes coming not from the user.

In order to achieve this we introduced a new PausableTextWatcher class which does not fire events while being paused.

class PausableTextWatcher(val listener: (text: Editable) -> Unit) : TextWatcher {

    private var active = true

    override fun afterTextChanged(s: Editable?) {
        if (active) {
            listener(s!!)
        }
    }

    fun pause(block: () -> Unit) = synchronized(this) {
        active = false
        block()
        active = true
    }
    
    ...
}

After replacing the old TextWatcher with the new one, we can now simply perform our edit operations like this:

textWatcher.pause {
	text.setSpan(...)
}

Learning 5: GBoard issues

After we had a working solution we somehow experienced performance issues with the keyboard. The keyboard app, in our case GBoard would start to react slowly and become completely unresponsive. Even worse: Not only our app was affected, the keyboard got unresponsive in all apps across the system.

Did our little app just nuke GBoard? Nuke

Further investigation revealed that the spans seemed to be the root cause. Digging deeper in the Android source we discovered that the ForegroundColorSpan which we used implemented ParcelableSpan. After some wild guessing we concluded that the GBoard app receives all text and all spans on every keystroke and couldn’t properly handle this amount of data. As a workaround we’re now using our own span class which does not implement the ParcelableSpan interface. This way our spans get ignored by the system and thus are never transferred to the GBoard app. After applying this code change - voila! - the GBoard app behaves normal again.

Summary

To summarize, styling EditTexts is definitely not straightforward and the topic of text itself is very exhaustive. Spans are a powerful concept which allow a lot of stylistic freedom while still being able to use standard widgets like EditText. It’s crazy (and wonderful) how many little details you need to know in order to master small parts of the Android framework.

I hope you enjoyed this small write-up. If you’re curious how the end product turned out, feel free to download Napkin from Google Play. If you have any feedback feel free to drop me a message!


All blog posts: