2626import static java .lang .Math .min ;
2727
2828import android .content .Context ;
29+ import android .content .res .TypedArray ;
2930import android .graphics .Canvas ;
3031import android .graphics .drawable .Drawable ;
32+ import android .os .SystemClock ;
3133import android .util .AttributeSet ;
3234import android .view .View ;
3335import android .view .ViewGroup ;
4244import androidx .annotation .RestrictTo .Scope ;
4345import androidx .annotation .VisibleForTesting ;
4446import com .google .android .material .color .MaterialColors ;
47+ import com .google .android .material .internal .ThemeEnforcement ;
4548import com .google .android .material .progressindicator .AnimatorDurationScaleProvider ;
4649import java .util .Arrays ;
4750
4851/** This class implements the loading indicators. */
4952public final class LoadingIndicator extends View implements Drawable .Callback {
5053 static final int DEF_STYLE_RES = R .style .Widget_Material3_LoadingIndicator ;
5154
55+ /**
56+ * The maximum time, in milliseconds, that the requested hide action is allowed to wait once
57+ * {@link #show()} is called.
58+ */
59+ static final int MAX_HIDE_DELAY = 1000 ;
60+
5261 @ NonNull private final LoadingIndicatorDrawable drawable ;
5362 @ NonNull private final LoadingIndicatorSpec specs ;
5463
64+ /**
65+ * The time, in milliseconds, that the loading indicator will wait to show once the component
66+ * becomes visible. If set to zero (as default) or negative values, the show action will start
67+ * immediately.
68+ */
69+ private final int showDelay ;
70+
71+ /**
72+ * The minimum time, in milliseconds, that the requested hide action will wait to start once
73+ * {@link #show()} is called. If set to zero or negative values, the requested hide action will
74+ * start as soon as {@link #hide()} is called. This value is capped to {@link #MAX_HIDE_DELAY}.
75+ *
76+ * @see #showDelay
77+ */
78+ private final int minHideDelay ;
79+
80+ private long lastShowStartTime = -1L ;
81+
5582 public LoadingIndicator (@ NonNull Context context ) {
5683 this (context , null );
5784 }
@@ -74,9 +101,81 @@ public LoadingIndicator(
74101 drawable .setCallback (this );
75102
76103 specs = drawable .getDrawingDelegate ().specs ;
104+
105+ TypedArray a =
106+ ThemeEnforcement .obtainStyledAttributes (
107+ context , attrs , R .styleable .LoadingIndicator , defStyleAttr , DEF_STYLE_RES );
108+ showDelay = a .getInt (R .styleable .LoadingIndicator_showDelay , -1 );
109+ int minHideDelayUncapped = a .getInt (R .styleable .LoadingIndicator_minHideDelay , -1 );
110+ minHideDelay = min (minHideDelayUncapped , MAX_HIDE_DELAY );
111+ a .recycle ();
112+
77113 setAnimatorDurationScaleProvider (new AnimatorDurationScaleProvider ());
78114 }
79115
116+ /**
117+ * Shows the loading indicator. If {@code showDelay} has been set to a positive value, wait until
118+ * the delay elapsed before starting the show action. Otherwise start showing immediately.
119+ */
120+ public void show () {
121+ if (showDelay > 0 ) {
122+ removeCallbacks (delayedShow );
123+ postDelayed (delayedShow , showDelay );
124+ } else {
125+ delayedShow .run ();
126+ }
127+ }
128+
129+ /**
130+ * Sets the visibility to {@code VISIBLE}. If this changes the visibility it will invoke {@code
131+ * onVisibilityChanged} and handle the visibility with animation of the drawables.
132+ *
133+ * @see #onVisibilityChanged(View, int)
134+ */
135+ private void internalShow () {
136+ if (minHideDelay > 0 ) {
137+ // The hide delay is positive, saves the time of starting show action.
138+ lastShowStartTime = SystemClock .uptimeMillis ();
139+ }
140+ setVisibility (VISIBLE );
141+ }
142+
143+ /**
144+ * Hides the loading indicator. If {@code minHideDelay} has been set to a positive value, wait
145+ * until the delay elapsed before starting the hide action. Otherwise start hiding immediately.
146+ */
147+ public void hide () {
148+ if (getVisibility () != VISIBLE ) {
149+ // No need to hide, as the component is still invisible.
150+ removeCallbacks (delayedShow );
151+ return ;
152+ }
153+
154+ removeCallbacks (delayedHide );
155+ long timeElapsedSinceShowStart = SystemClock .uptimeMillis () - lastShowStartTime ;
156+ boolean enoughTimeElapsed = timeElapsedSinceShowStart >= minHideDelay ;
157+ if (enoughTimeElapsed ) {
158+ delayedHide .run ();
159+ return ;
160+ }
161+ postDelayed (delayedHide , /* delayMillis= */ minHideDelay - timeElapsedSinceShowStart );
162+ }
163+
164+ /**
165+ * If the component uses {@link DrawableWithAnimatedVisibilityChange} and needs to be hidden with
166+ * animation, it will trigger the drawable to start the hide animation. Otherwise, it will
167+ * directly set the visibility to {@code INVISIBLE}.
168+ *
169+ * @see #hide()
170+ */
171+ private void internalHide () {
172+ getDrawable ().setVisible (/* visible= */ false , /* restart= */ false , /* animate= */ true );
173+
174+ if (!getDrawable ().isVisible ()) {
175+ setVisibility (INVISIBLE );
176+ }
177+ }
178+
80179 @ Override
81180 protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
82181 int widthMode = MeasureSpec .getMode (widthMeasureSpec );
@@ -142,6 +241,14 @@ protected void onWindowVisibilityChanged(int visibility) {
142241 visibleToUser (), /* restart= */ false , /* animate= */ visibility == VISIBLE );
143242 }
144243
244+ @ Override
245+ protected void onAttachedToWindow () {
246+ super .onAttachedToWindow ();
247+ if (visibleToUser ()) {
248+ internalShow ();
249+ }
250+ }
251+
145252 @ Override
146253 public void invalidateDrawable (@ NonNull Drawable drawable ) {
147254 invalidate ();
@@ -318,4 +425,33 @@ public void setAnimatorDurationScaleProvider(
318425 @ NonNull AnimatorDurationScaleProvider animatorDurationScaleProvider ) {
319426 drawable .animatorDurationScaleProvider = animatorDurationScaleProvider ;
320427 }
428+
429+ // ************************ In-place defined parameters ****************************
430+
431+ /**
432+ * The runnable, which executes the start action. This is used to schedule delayed show actions.
433+ *
434+ * @see #show()
435+ */
436+ private final Runnable delayedShow =
437+ new Runnable () {
438+ @ Override
439+ public void run () {
440+ internalShow ();
441+ }
442+ };
443+
444+ /**
445+ * The runnable, which executes the hide action. This is used to schedule delayed hide actions.
446+ *
447+ * @see #hide()
448+ */
449+ private final Runnable delayedHide =
450+ new Runnable () {
451+ @ Override
452+ public void run () {
453+ internalHide ();
454+ lastShowStartTime = -1L ;
455+ }
456+ };
321457}
0 commit comments