Skip to content

Conversation

@jayyaj12
Copy link

Summary

Fix corner morph animation regression in MaterialButtonGroup introduced after commit ea9d250.

Problem

After ea9d250, corner morph animations in MaterialButtonGroup are interrupted during state changes (pressed, focused, etc.).

Root cause:
In MaterialShapeDrawable.onBoundsChange(), animations are now always skipped:

// After ea9d25050
updateShape(getState(), /* skipAnimation= */ true);  // Always true!

This causes springAnimatedCornerSizes to be immediately set to target values, interrupting any ongoing animations.

Reproduction:

  1. Use MaterialButtonGroup with stateful corner shapes
  2. Press a button
  3. Observe: corner animation is interrupted (not smooth)

Why MaterialButtonGroup Cannot Fix This

I initially attempted to fix this in MaterialButtonGroup by:

  1. Caching shapes to avoid redundant setShapeAppearance() calls
  2. Comparing corner sizes before applying shapes
  3. One-time initialization to prevent re-setting shapes

However, all approaches failed because:

// MaterialButton state change flow:
Button.setPressed(true)
  → Drawable.setState(new int[]{android.R.attr.state_pressed})
    → MaterialShapeDrawable.onStateChange(state)
      → updateShape(state, skipAnimation=false)  // Animation requested
        → [Animation starts...]
    → MaterialShapeDrawable.onBoundsChange(bounds)  // Called by frameworkupdateShape(state, skipAnimation=true)     // Animation force-skipped!springAnimatedCornerSizes = targetValues  // Animation interrupted

The critical issue:

  • onBoundsChange() is called by the Android framework after state changes
  • With skipAnimation=true hardcoded, there's no way for MaterialButtonGroup to preserve animations
  • Any attempt at the MaterialButtonGroup level is overridden by onBoundsChange()

Why bounds change during state changes:

  • Ripple effects may adjust drawable bounds
  • Elevation changes affect shadow bounds
  • Padding adjustments for pressed states
  • Even if bounds values are identical, onBoundsChange() is still called

Solution

The fix must be in MaterialShapeDrawable to distinguish between:

  • Initial layout (first non-empty bounds) → Skip for performance
  • Subsequent calls (state changes, etc.) → Allow animation for smooth UX

Restore the boundsIsEmpty flag approach from alpha06:

private boolean boundsIsEmpty = true;

protected void onBoundsChange(Rect bounds) {
    // ...
    if (drawableState.shapeAppearance.isStateful() && !bounds.isEmpty()) {
        // Skip only on FIRST non-empty bounds
        updateShape(getState(), /* skipAnimation= */ boundsIsEmpty);
    }
    boundsIsEmpty = bounds.isEmpty();
}

Why this works:

  • First call (initial layout): boundsIsEmpty=true → Skip (fast initialization)
  • Subsequent calls: boundsIsEmpty=false → Animate (smooth UX)
  • No way to break this from external code

Alternative Approaches Considered

❌ Option 1: Fix in MaterialButtonGroup

Tried: Cache shapes, prevent redundant updates
Failed: onBoundsChange() always overrides with skipAnimation=true

❌ Option 2: Track actual bounds changes

Tried: Compare previousBounds to detect real size changes
Failed: Bounds may change slightly during state transitions (ripple, elevation)

✅ Option 3: Restore boundsIsEmpty logic (this PR)

Works: Distinguishes initial layout from subsequent calls at the source

Implementation

Testing

  • ✅ Verified smooth corner animations in MaterialButtonGroup during state changes
  • ✅ Confirmed no performance regression on initial layout
  • ✅ Tested across multiple state transitions (pressed, focused, disabled)
  • ✅ Verified no impact on other components using MaterialShapeDrawable

Changes

Modified:

  • lib/java/com/google/android/material/shape/MaterialShapeDrawable.java
    • Restored boundsIsEmpty flag
    • Updated onBoundsChange() animation skip logic

Fixes #4990


Discussion

@pekingme I understand the intent of ea9d250 was to improve performance by skipping animations during bounds changes. However, this also affects state-only changes where animation should be preserved.

Would you be open to this refinement that maintains the performance benefit for initial layout while allowing animations for state transitions?

Fix regression introduced in ea9d250 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 material-components#4990
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Button] Corner morph animation broken in ButtonGroup

2 participants