material-components/material-components-android · error · IllegalArgumentException

secondaryIconGravity cannot have the same alignment as iconG

Error message

secondaryIconGravity cannot have the same alignment as iconGravity

What it means

MaterialButton supports a primary icon (iconGravity) and an expanded-layout secondary icon (secondaryIconGravity), but both drawables are placed into the same set of compound drawable slots. If both gravities resolve to the same alignment (start/start, end/end, or top/top), the two icons would fight over one compound-drawables position and one would silently overwrite the other. validateSecondaryIconGravity() therefore throws IllegalArgumentException whenever icon != null && secondaryIcon != null && areIconsGravitySameAlignment() (MaterialButton.java:1263 checks isIconStart&&isSecondaryIconStart, isIconEnd&&isSecondaryIconEnd, isIconTop&&isSecondaryIconTop). The library prefers failing loudly over rendering a wrong icon.

Source

Thrown at lib/java/com/google/android/material/button/MaterialButton.java:1359

            || (isSecondaryIconTop() && drawableTop != secondaryIcon);
    // Force icon update if needsIconReset otherwise updated it only if icon has changed.
    if (needsIconReset || hasIconChanged) {
      if (isSecondaryIconStart()) {
        setCompoundDrawablesRelative(
            secondaryIcon, getUpdatedSecondaryIconFor(1), getUpdatedSecondaryIconFor(2), null);
      } else if (isSecondaryIconEnd()) {
        setCompoundDrawablesRelative(
            getUpdatedSecondaryIconFor(0), getUpdatedSecondaryIconFor(1), secondaryIcon, null);
      } else if (isSecondaryIconTop()) {
        setCompoundDrawablesRelative(
            getUpdatedSecondaryIconFor(0), secondaryIcon, getUpdatedSecondaryIconFor(2), null);
      }
    }
  }

  private void validateSecondaryIconGravity() {
    if (secondaryIcon != null && icon != null && areIconsGravitySameAlignment()) {
      throw new IllegalArgumentException(
          "secondaryIconGravity cannot have the same alignment as iconGravity");
    }
  }

  @Nullable
  private Drawable getUpdatedSecondaryIconFor(int position) {
    switch (position) {
      case 0: // start
        return (icon != null && isIconStart()) ? icon : null;
      case 1: // top
        return (icon != null && isIconEnd()) ? icon : null;
      case 2: // end
        return (icon != null && isIconEnd()) ? icon : null;
      default:
        return null;
    }
  }

View on GitHub (pinned to ac7e18efee)

Solutions

  1. Pick different alignments for the two icons, e.g. keep iconGravity="start" (or "textStart") and set app:secondaryIconGravity="end" / call setSecondaryIcon(GRAVITY_END).
  2. If both icons genuinely belong on the same side, merge them into a single layered drawable (LayerDrawable) and set it as the primary icon with only one gravity.
  3. Drop one of the icons: if the secondary icon is not needed for the collapsed/expanded affordance, call setSecondaryIcon(null) before changing gravities.
  4. When toggling gravities at runtime, always set the new secondaryIconGravity in the same transaction as the icon change and test all state combinations (icon-only, secondary-only, both).

Example fix

// before
button.setIcon(AppCompatResources.getDrawable(this, R.drawable.leading));
button.setIconGravity(MaterialButton.ICON_GRAVITY_START);
button.setSecondaryIcon(AppCompatResources.getDrawable(this, R.drawable.expand));
button.setSecondaryIconGravity(MaterialButton.ICON_GRAVITY_START); // throws

// after
button.setIcon(AppCompatResources.getDrawable(this, R.drawable.leading));
button.setIconGravity(MaterialButton.ICON_GRAVITY_START);
button.setSecondaryIcon(AppCompatResources.getDrawable(this, R.drawable.expand));
button.setSecondaryIconGravity(MaterialButton.ICON_GRAVITY_END);
Defensive patterns

Strategy: validation

Validate before calling

// Before setting both icons, confirm alignments differ
boolean sameAlignment =
    (button.getIconGravity() == MaterialButton.ICON_GRAVITY_START
        && secondaryGravity == MaterialButton.ICON_GRAVITY_START)
 || (button.getIconGravity() == MaterialButton.ICON_GRAVITY_END
        && secondaryGravity == MaterialButton.ICON_GRAVITY_END)
 || (button.getIconGravity() == MaterialButton.ICON_GRAVITY_TOP
        && secondaryGravity == MaterialButton.ICON_GRAVITY_TOP);
if (!sameAlignment) {
  button.setSecondaryIconGravity(secondaryGravity);
}

Try / catch

try { button.setSecondaryIcon(drawable); } catch (IllegalArgumentException e) { Log.w(TAG, "icon gravities collide; using primary icon only"); }

Prevention

When it happens

Trigger: Calling setIcon() and setSecondaryIcon() (or inflating app:icon plus app:secondaryIcon from XML) while iconGravity and secondaryIconGravity share an alignment — e.g. iconGravity="start" with secondaryIconGravity="start", or both set to TEXT_GRAVITY_START-like values that map to the same edge. Also triggered later by setSecondaryIconGravity(...) or setIconGravity(...) that moves one icon onto the other's edge while both drawables are non-null.

Common situations: Copy-pasting a MaterialButton XML block for the expandable/collapsible button style and only changing app:secondaryIcon, leaving both gravity attributes defaulted or identical. Setting the secondary icon gravity programmatically after restoring state (e.g. in onBindViewHolder for a recycling view) without re-checking the primary gravity. Version upgrades where the secondary-icon API (used with MotionLayout expand transitions) started enforcing this constraint.

Related errors


AI-assisted analysis of material-components/material-components-android@ac7e18efee (2026-08-14). Data as JSON: /api/errors/7959e007c2fd35e9. Report an issue: GitHub.