Search code examples
androidprogress-barloadingandroid-progressbarmaterial-components-android

TextInputLayout with loading indicator


Using TextInputLayout from Material Design library we can use various end icon modes for password redaction, text clearing and custom mode. Furthermore, if we use any of Widget.MaterialComponents.TextInputLayout.*.ExposedDropdownMenu styles it will automatically apply special end icon mode that displays open and close chevrons.

Example of various icon modes:

End Icon Modes

Given the variety of use cases for the end icon, we decided to use a loading indicator in the InputTextLayout so that it looks like this:

Loading indicator mode

How should one proceed to implement it?


Solution

  • 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:

    InputTextLayout with loading indicators

    with android.R.attr.progressBarStyle (medium) and android.R.attr.progressBarStyleSmall respectively.