diff --git a/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java b/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java index e6bd0ba7d4d..bc5789a11c2 100644 --- a/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java +++ b/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java @@ -47,6 +47,7 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Looper; +import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import androidx.annotation.AttrRes; @@ -185,6 +186,12 @@ public CornerSize apply(@NonNull CornerSize cornerSize) { private boolean shadowBitmapDrawingEnable = true; // Variables for corner morph. + // Tracks if this is the first non-empty bounds (for skipping initial animation). + private boolean boundsIsEmpty = true; + // Timestamp when state change occurred, used to allow animation during state transitions. + private long stateChangeTimestamp = 0; + // Duration (ms) to allow animation after state change, accounting for animation frames. + private static final long STATE_CHANGE_ANIMATION_WINDOW_MS = 500; private boolean isRoundRectCornerMorph = true; @NonNull private ShapeAppearanceModel strokeShapeAppearanceModel; @Nullable private SpringForce cornerSpringForce; @@ -1109,10 +1116,19 @@ protected void onBoundsChange(Rect bounds) { pathDirty = true; strokePathDirty = true; super.onBoundsChange(bounds); + if (drawableState.shapeAppearance.isStateful() && !bounds.isEmpty()) { - // When bounds change, we want to snap to the new shape without animation. - updateShape(getState(), /* skipAnimation= */ true); + // Check if we're within the animation window after a state change. + long timeSinceStateChange = SystemClock.elapsedRealtime() - stateChangeTimestamp; + boolean isWithinStateChangeWindow = timeSinceStateChange < STATE_CHANGE_ANIMATION_WINDOW_MS; + + // Skip animation if this is the first non-empty bounds OR outside state change window. + // Allow animation if we're within the window after a state change. + boolean skipAnimation = boundsIsEmpty || !isWithinStateChangeWindow; + updateShape(getState(), skipAnimation); } + + boundsIsEmpty = bounds.isEmpty(); } @Override @@ -1537,6 +1553,8 @@ public boolean isStateful() { @Override protected boolean onStateChange(int[] state) { if (drawableState.shapeAppearance.isStateful()) { + // Record state change timestamp to allow animation within the time window. + stateChangeTimestamp = SystemClock.elapsedRealtime(); updateShape(state); } boolean paintColorChanged = updateColorsForState(state);