Autosizing Fonts for Android EditTexts

When designing a screen layout, it’s necessary to think about what data is displayed/entered where and how much space is necessary to cover most use cases. And if that data exceeds the allocated space, there are multiple ways to configure a text field using end-ellipsis, scrollbar, or multiline properties. In fact, the latter may even dynamically increase the occupied space.

But sometimes, that’s not a viable path, and we want to do things the other way around and have our content match the allocated space.

Google addressed this issue with the introduction of autosizing TextViews in Android 8.0, but unfortunately only partially, as it covered TextView but not EditText. So in this blog post, I’ll walk through how to autosize TextViews and how to transfer that autosizing capability to EditTexts.

Autosizing TextViews

The name autosizing is a bit misleading, since it’s not the TextView that’s autosized, but rather the font. This is done namely to fit the displayed text (more or less) exactly into the TextView’s allocated space. There are even some tweaking options, which allow us to set a minimum and maximum text size and the shrink/stretch granularity.

To make it work, all we have to do is set our TextView’s autosizeTextType property to uniform. And suddenly, instead of the following:

Text that’s too large

We’ll have this:

Text that’s autosized

Neat.

💡 Tip: Dynamic heights and widths are the enemy when it comes to autosizing. We’ll most likely want a specific width, so we set it to a fixed value (match_parent is fine). Also, restricting the maxLines property is a great idea — we don’t want line breaks to sabotage our autosizing efforts. The properties for a single-line autosizing TextView might look like this:

android:width="match_parent"
android:height="40dp"
android:maxLines="1"
app:autoSizeTextType="uniform"

Halfway There

Cool, we’ve successfully autosized TextViews. But we’re only halfway there.

Why only halfway? Sooner or later, the circumstances will require an autosizing EditText. One may think EditText is a direct descendant of TextView, and as such, that the autosizing functionality is inherited. Well… wishful thinking. For some awkward reason, it’s restricted to TextView only.

You may be wondering “What now?” Well, a few years ago, someone told me a good developer is lazy, which mostly translates to: Reuse existing resources and don’t reinvent the wheel.

OK. Lazy approach it is.

Since we already learned a TextView does a decent autosizing job, we’ll use one as a tool to perform the required calculations for us.

The basic idea is to wrap our existing EditText into a container with identical layout parameters and add an invisible autosizing TextView in there too. Ideally, these changes don’t have any impact on the visual representation of our layout.

That being said, since we’re going to manipulate the UI layout, it might lead to complications if we have any direct layout references to our EditText (especially if it’s part of a ConstraintLayout or RelativeLayout). In that case, we should wrap the EditText in an extra parent container that can be safely referenced by other widgets.

To help with this, I’ll break it down into three steps:

  1. Create a FrameLayout, remove the EditText from the layout hierarchy, replace it with the FrameLayout, and put the EditText into it.

  2. Create an invisible autosizing TextView with layout parameters identical to EditText and add it to the FrameLayout too. Now we have both text widgets — identically laid out — within the FrameLayout.

  3. Install a TextWatcher, which observes inputs to EditText. Each input is directly applied to the TextView, which recalculates the optimal text size to be applied to the EditText. One thing to note is that the TextView doesn’t recalculate the new font size immediately when we assign a new text to it, as the recalculation involves laying things out. The easiest way to handle this behavior is to delay the new size query with a post call.

Let’s pack this all together in a small utility class:

object EditTextAutoSizeUtility{

    /**
     * This function adds font autosizing to the provided `[editText]` by utilizing an invisible
     * autosizing `TextView`. During this process, the `EditText` is replaced within the layout
     * hierarchy with `FrameLayout`, which contains both the original `EditText` and the autosizing
     * `TextView`.
     *
     * @param`EditText`... the `EditText` you intend to make autosizing.
     * @param context ... your active context, used to create the `FrameLayout` and the `TextView`.
     * @return ... the newly created `Framelayout`, just in case you need it.
     */
    fun setupAutoResize(editText: EditText, context: Context): FrameLayout {
        // Step 1 — Create `FrameLayout` and put the `EditText` into it.
        val container = FrameLayout(context)
        val orgLayoutParams = editText.layoutParams

        (editText.parent as? ViewGroup)?.let { editParent ->
            editParent.indexOfChild(editText).let { index ->
                editParent.removeViewAt(index)
                container.addView(
                    editText,
                    FrameLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                    )
                )
                editParent.addView(container, index, orgLayoutParams)
            }
        }

        // Step 2 — Create the invisible autosizing `TextView` and add it to the `FrameLayout`.
        val textView = createAutoSizeHelperTextView(editText, context)
        container.addView(textView, 0, editText.layoutParams)

        // Step 3 — Install a listener to keep `TextView` and `EditText` in sync.
        editText.addTextChangedListener(object : TextWatcher {
            val originalTextSize = editText.textSize

            // Apply the changed text to the `TextView` and its new calculated `textSize` to the `EditText`.
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                textView.setText(s?.toString(), TextView.BufferType.EDITABLE)
                // `textView` lays itself out again, so delay the query of the new `textSize` by using `post{ ... }`.
                editText.post {
                    val optimalSize =
                        if (s.isNullOrBlank())
                            originalTextSize
                        else {
                            val autosize = textView.textSize
                            autosize
                        }
                    editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, optimalSize)
                }
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun afterTextChanged(editable: Editable?) {}
        })

        return container
    }

    /**
     * Creates the invisible `TextView` we use for the `textSize` calculation. It uses the same
     * padding as the `EditText`, since we need both with matching sizes to yield the best possible
     * `textSize` results.
     */
    private fun createAutoSizeHelperTextView(editText: EditText, context: Context): TextView =
        TextView(context).apply {
            maxLines = 1
            visibility = View.INVISIBLE
            TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
                this,
                spToPx(context, AUTOSIZE_EDITTEXT_MINTEXTSIZE_SP),
                // It's a good idea to set the helper's max `textSize` to the initial text size of the `EditText` to avoid excessively inflating the font size.
                editText.textSize.roundToInt(),
                spToPx(context, AUTOSIZE_EDITTEXT_STEPSIZE_GRANULARITY_SP),
                TypedValue.COMPLEX_UNIT_PX
            )
            // Ensure `autosizeHelper` has the same layout parameters as the `EditText`.
            setPadding(
                editText.paddingLeft,
                editText.paddingTop,
                editText.paddingRight,
                editText.paddingBottom
            )
        }

    private const val AUTOSIZE_EDITTEXT_MINTEXTSIZE_SP = 12f
    private const val AUTOSIZE_EDITTEXT_STEPSIZE_GRANULARITY_SP = 1f

    fun spToPx(context: Context, sp: Float) =
        TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, sp, context.resources.displayMetrics
        ).toInt()
}

Et Voilà

When initializing our EditText, now all we need to call is:

// Add autosizing capabilities to our input field.
EditTextAutoSizeUtility.setupAutoResize(editText, context)

And we’re done.

Final Thoughts

Admittedly, the solution isn’t perfect. If you look closely, as soon as the resizing starts, there’s a slight flickering noticeable on the left side, which is due to the unavoidable delay from posting setTextSize(...) instead of calling it directly. Nonetheless, if you’re not too picky, I think it’s an acceptable compromise until — if ever — native support is available.