带有加载指示器的 TextInputLayout

TextInputLayout with loading indicator

使用TextInputLayout from Material Design library we can use various end icon modes进行密码编辑、文本清除和自定义模式。此外,如果我们使用任何 Widget.MaterialComponents.TextInputLayout.*.ExposedDropdownMenu 样式,它会自动应用显示打开和关闭 V 形的特殊结束图标模式。

各种图标模式示例:

考虑到结束图标的各种用例,我们决定在 InputTextLayout 中使用加载指示器,使其看起来像这样:

应该如何着手实施?

可以像这样简单地设置使用自定义可绘制对象来代替结束图标:

textInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM
textInputLayout.endIconDrawable = progressDrawable

有问题的部分是获取可绘制的加载指示器。


选项 1 错误

没有 public 个可绘制资源可用于加载指示器。

android.R.drawable.progress_medium_material 但它被标记为私有,无法在代码中解析。将资源及其所有依赖的私有资源复制到大约 6 个文件中(2 个可绘制对象 + 2 个动画师 + 2 个插值器)。这可能行得通,但感觉很像黑客。


选项 2 错误

我们可以使用ProgressBar来检索它的indeterminateDrawable。这种方法的问题在于可绘制对象与 ProgressBar 紧密相关。指示器仅在 ProgressBar 可见时才会显示动画,对一个视图进行着色也会对另一个视图中的指示器进行着色,并且可能会出现其他奇怪的行为。

在类似的情况下,我们可以使用Drawable.mutate()来获取drawable的新副本。不幸的是 indeterminateDrawable 已经发生变异,因此 mutate() 什么都不做。

真正将可绘制对象与 ProgressBar 分离的是对 indeterminateDrawable.constantState.newDrawable() 的调用。请参阅 documentation 了解更多信息。

无论如何,这仍然感觉像是一个 hack。


好的选项 3

虽然可绘制资源标记为私有,但我们可以解析某些主题属性以获取系统默认加载指示可绘制资源。主题定义了引用 ProgressBar 样式的 progressBarStyle 属性。此样式内部是引用主题可绘制对象的 indeterminateDrawable 属性。在代码中,我们可以像这样解析可绘制对象:

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
}

太好了,现在我们有了一个无需修改即可绘制的本机加载指示器!


额外措施

动画

现在,如果您将可绘制对象插入此代码

textInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM
textInputLayout.endIconDrawable = progressDrawable

你会发现它不显示任何东西。

实际上,它确实正确显示了可绘制对象,但真正的问题是它没有被动画化。碰巧在动画开始时 drawable 折叠成一个不可见的点。

对我们来说不幸的是,我们无法将 drawable 转换为它的真实类型 AnimationScaleListDrawable 因为它在 com.android.internal.graphics.drawable 包中。 对我们来说幸运的是,我们可以将其键入 Animatablestart() it:

(drawable as? Animatable)?.start()

颜色

TextInputLayout receives/loses 焦点时会发生另一个意外行为。在这种情况下,它会根据 layout.setEndIconTintList() 定义的颜色为可绘制对象着色。如果您没有明确指定色调列表,它会将可绘制对象着色为 ?colorPrimary。但是在我们设置 drawable 的那一刻,它仍然被着色为 ?colorAccent 并且在一个看似随机的时刻它会改变颜色。

出于这个原因,我建议使用相同的 ColorStateListlayout.endIconTintListdrawable.tintList 进行染色。如:

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)

最终我们得到这样的结果:

android.R.attr.progressBarStyle(中)和 android.R.attr.progressBarStyleSmall 分别。

您可以使用 Material 组件库提供的 ProgressIndicator

在您的布局中只需使用:

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textinputlayout"
        ...>
        
        <com.google.android.material.textfield.TextInputEditText
          .../>
        
    </com.google.android.material.textfield.TextInputLayout>

然后定义 ProgressIndicator 使用:

    ProgressIndicatorSpec progressIndicatorSpec = new ProgressIndicatorSpec();
    progressIndicatorSpec.loadFromAttributes(
            this,
            null,
            R.style.Widget_MaterialComponents_ProgressIndicator_Circular_Indeterminate);

    progressIndicatorSpec.circularInset = 0; // Inset
    progressIndicatorSpec.circularRadius =
            (int) dpToPx(this, 10); // Circular radius is 10 dp.

    IndeterminateDrawable progressIndicatorDrawable =
            new IndeterminateDrawable(
                    this,
                    progressIndicatorSpec,
                    new CircularDrawingDelegate(),
                    new CircularIndeterminateAnimatorDelegate());

最后将可绘制对象应用于 TextInputLayout:

 textInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM);
 textInputLayout.setEndIconDrawable(progressIndicatorDrawable);

转为dp的util方法:

public static float dpToPx(@NonNull Context context, @Dimension(unit = Dimension.DP) int dp) {
    Resources r = context.getResources();
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
}

您可以轻松自定义 circularRadiusindicatorColors 以及 ProgressIndicator 中定义的所有其他属性:

    progressIndicatorSpec.indicatorColors = getResources().getIntArray(R.array.progress_colors);
    progressIndicatorSpec.growMode = GROW_MODE_OUTGOING;

使用这个数组:

<integer-array name="progress_colors">
    <item>@color/...</item>
    <item>@color/....</item>
    <item>@color/....</item>
</integer-array>

注意: 至少需要 1.3.0-alpha02.

版本

这个问题已经有了很好的答案,但是,我想post一个更简洁更简单的解决方案。如果你使用androidx你有一个class继承了Drawable——CircularProgressDrawable,所以你可以使用它。这是我在项目中使用的一段代码:

CircularProgressDrawable drawable = new CircularProgressDrawable(requireContext());
        drawable.setStyle(CircularProgressDrawable.DEFAULT);
        drawable.setColorSchemeColors(Color.GREEN);
 inputLayout.setEndIconOnClickListener(view -> {
            inputLayout.setEndIconDrawable(drawable);
            drawable.start();
//some long running operation starts...
}

结果: