From d847cb5c2ee2ad66be142e120eb8e7634e4841e6 Mon Sep 17 00:00:00 2001 From: jayyaj12 Date: Wed, 14 Jan 2026 18:32:33 +0900 Subject: [PATCH 1/2] [MaterialShapeDrawable] Restore alpha06 corner animation behavior Fix regression introduced in ea9d25050 where corner morph animations in MaterialButtonGroup were interrupted. The issue was that onBoundsChange() always skipped animations by passing `skipAnimation=true` to updateShape(). This affected state-only changes (like button press) where bounds don't actually change. Restored the boundsIsEmpty logic from 1.14.0-alpha06: - Skip animation only on first non-empty bounds (initial layout) - Allow animations for subsequent state changes (smooth UX) Fixes #4990 --- .../android/material/shape/MaterialShapeDrawable.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java b/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java index e6bd0ba7d4d..5420bf5d68c 100644 --- a/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java +++ b/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java @@ -185,6 +185,8 @@ 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; private boolean isRoundRectCornerMorph = true; @NonNull private ShapeAppearanceModel strokeShapeAppearanceModel; @Nullable private SpringForce cornerSpringForce; @@ -1110,9 +1112,10 @@ protected void onBoundsChange(Rect bounds) { 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); + // Skip animation only on first non-empty bounds; allow animation on subsequent state changes. + updateShape(getState(), /* skipAnimation= */ boundsIsEmpty); } + boundsIsEmpty = bounds.isEmpty(); } @Override From d54646d76e083d357102d84c06a7e231d7a020c8 Mon Sep 17 00:00:00 2001 From: jayyaj12 Date: Mon, 26 Jan 2026 10:58:19 +0900 Subject: [PATCH 2/2] [Shape] Fix corner animation interruption during state changes while maintaining performance. After ea9d25050, corner morph animations in MaterialButtonGroup were interrupted during state changes because onBoundsChange() always skipped animations. This caused abrupt visual transitions when buttons were pressed or focused. Simply reverting would reintroduce unnecessary animations during layout changes (screen rotation, window resize), hurting performance. Solution: Use time-based approach to distinguish change types: - Track timestamp when state changes occur (onStateChange) - Allow animation for 500ms after state change - Skip animation for pure layout changes (outside time window) This maintains both smooth state transition UX and layout change performance. Fixes #4990 --- .../material/shape/MaterialShapeDrawable.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java b/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java index 5420bf5d68c..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; @@ -187,6 +188,10 @@ public CornerSize apply(@NonNull CornerSize cornerSize) { // 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; @@ -1111,10 +1116,18 @@ protected void onBoundsChange(Rect bounds) { pathDirty = true; strokePathDirty = true; super.onBoundsChange(bounds); + if (drawableState.shapeAppearance.isStateful() && !bounds.isEmpty()) { - // Skip animation only on first non-empty bounds; allow animation on subsequent state changes. - updateShape(getState(), /* skipAnimation= */ boundsIsEmpty); + // 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(); } @@ -1540,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);