One can simply set use custom drawable in place of End Icon like this:
textInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM
textInputLayout.endIconDrawable = progressDrawable
The problematic part is getting hold of a loading indicator drawable.
Bad Option 1
There is no public drawable resource we can use for a loading indicator.
There is android.R.drawable.progress_medium_material
but it is marked private and cannot be resolved in code. Copying the resource and all of its dependent private resources totals into about 6 files (2 drawables + 2 animators + 2 interpolators). That could work but feels quite like a hack.
Bad Option 2
We can use ProgressBar
to retrieve its indeterminateDrawable
. The problem with this approach is that the drawable is closely tied to the ProgressBar
. The indicator is animated only when the ProgressBar
is visible, tinting one View will also tint the indicator in the other View and probably additional weird behavior.
In similar situations, we can use Drawable.mutate()
to get a new copy of the drawable. Unfortunately the indeterminateDrawable
is already mutated and thus mutate()
does nothing.
What actually worked to decouple the drawable from the ProgressBar
was a call to indeterminateDrawable.constantState.newDrawable()
. See documentation for more insight.
Anyway, this still feels like a hack.
Good Option 3
Although the drawable resource is marked private we can resolve certain theme attributes to get the system's default loading indicator drawable. The theme defines progressBarStyle
attribute that references style for ProgressBar
. Inside of this style is indeterminateDrawable
attribute that references themed drawable. In code we can resolve the drawable like this:
fun Context.getProgressBarDrawable(): Drawable {
val value = TypedValue()
theme.resolveAttribute(android.R.attr.progressBarStyleSmall, value, false)
val progressBarStyle = value.data
val attributes = intArrayOf(android.R.attr.indeterminateDrawable)
val array = obtainStyledAttributes(progressBarStyle, attributes)
val drawable = array.getDrawableOrThrow(0)
array.recycle()
return drawable
}
Great, now we have a native loading indicator drawable without hacks!
Extra measures
Animation
Now if you plug in the drawable into this code
textInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM
textInputLayout.endIconDrawable = progressDrawable
you will find out that it does not display anything.
Actually, it does display the drawable correctly but the real problem is that it is not being animated. It just happens that at the beginning of the animation the drawable is collapsed into an invisible point.
Unfortunately for us, we cannot convert the drawable to its real type AnimationScaleListDrawable
because it is in com.android.internal.graphics.drawable
package.
Fortunately for us, we can type it as Animatable
and start()
it:
(drawable as? Animatable)?.start()
Colors
Another unexpected behavior happens when TextInputLayout
receives/loses focus. At such moments it will tint the drawable according to colors defined by layout.setEndIconTintList()
. If you don't explicitly specify a tint list, it will tint the drawable to ?colorPrimary
. But at the moment when we set the drawable, it is still tinted to ?colorAccent
and at a seemingly random moment it will change color.
For that reason I recommend to tint both layout.endIconTintList
and drawable.tintList
with the same ColorStateList
. Such as:
fun Context.fetchPrimaryColor(): Int {
val array = obtainStyledAttributes(intArrayOf(android.R.attr.colorPrimary))
val color = array.getColorOrThrow(0)
array.recycle()
return color
}
...
val states = ColorStateList(arrayOf(intArrayOf()), intArrayOf(fetchPrimaryColor()))
layout.setEndIconTintList(states)
drawable.setTintList(states)
Ultimately we get something like this:
with android.R.attr.progressBarStyle
(medium) and android.R.attr.progressBarStyleSmall
respectively.