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 TextView
s 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 TextView
s and how to transfer that autosizing capability to EditText
s.
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:
We’ll have this:
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 TextView
s. 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:
-
Create a
FrameLayout
, remove theEditText
from the layout hierarchy, replace it with theFrameLayout
, and put theEditText
into it. -
Create an invisible autosizing
TextView
with layout parameters identical toEditText
and add it to theFrameLayout
too. Now we have both text widgets — identically laid out — within theFrameLayout
. -
Install a
TextWatcher
, which observes inputs toEditText
. Each input is directly applied to theTextView
, which recalculates the optimal text size to be applied to theEditText
. One thing to note is that theTextView
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 apost
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.
Michael has a thing for architecture (slick software and unconventional buildings). If he’s not sitting in front of a computer or watching a series, you’ll find him in a CrossFit gym, in the kitchen, or at some Burning Man event.