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

Keylines after the last focal keyline must be ordered by dec

Error message

Keylines after the last focal keyline must be ordered by decreasing masked item size.

What it means

Mirror of error 38: after the last focal keyline, masked sizes must decrease monotonically toward the far edge (KeylineState.java:421). When a focal run has already been added (tmpLastFocalKeyline != null) and a subsequent non-focal keyline's maskedItemSize is larger than the previous one (lastKeylineMaskedSize), the builder throws. The trailing keylines shrink away from the full-size focal center, and any increase breaks the expected layout math.

Source

Thrown at lib/java/com/google/android/material/carousel/KeylineState.java:421

        if (lastFocalKeylineIndex != NO_INDEX && tmpKeylines.size() - lastFocalKeylineIndex > 1) {
          throw new IllegalArgumentException(
              "Keylines marked as focal must be placed next to each other. There cannot be"
                  + " non-focal keylines between focal keylines.");
        }
        if (maskedItemSize != tmpFirstFocalKeyline.maskedItemSize) {
          throw new IllegalArgumentException(
              "Keylines that are marked as focal must all have the same masked item size.");
        }
        tmpLastFocalKeyline = tmpKeyline;
        lastFocalKeylineIndex = tmpKeylines.size();
      } else {
        if (tmpFirstFocalKeyline == null && tmpKeyline.maskedItemSize < lastKeylineMaskedSize) {
          throw new IllegalArgumentException(
              "Keylines before the first focal keyline must be ordered by incrementing masked item"
                  + " size.");
        } else if (tmpLastFocalKeyline != null
            && tmpKeyline.maskedItemSize > lastKeylineMaskedSize) {
          throw new IllegalArgumentException(
              "Keylines after the last focal keyline must be ordered by decreasing masked item"
                  + " size.");
        }
      }
      lastKeylineMaskedSize = tmpKeyline.maskedItemSize;
      tmpKeylines.add(tmpKeyline);
      return this;
    }

    /**
     * Adds a keyline along the scrolling axis where an object should be masked by the given {@code
     * mask} and positioned at {@code offsetLoc}.
     *
     * <p>Note that calls to {@link #addKeyline(float, float, float, boolean, boolean)} and {@link
     * #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
     * keylines should be added in order of ascending {@code offsetLoc}. The first and last keylines
     * added are 'anchor' keylines that mark the start and ends of the keylines. These keylines do
     * not shift when scrolled.

View on GitHub (pinned to ac7e18efee)

Solutions

  1. Order trailing keylines descending in maskedItemSize (largest just after the focal run, smallest at the end).
  2. If generating keylines symmetrically, build the leading half ascending and mirror it (reverse) for the trailing half.
  3. Unit-test the strategy's KeylineState build with the library's own assertions so ordering regressions surface in CI, not on device.

Example fix

// before
builder.addKeylineRange(of0, of1, itemSize, itemSize, true, false); // focal run
builder.addKeyline(o3, 0.4f, size*0.5f, false, false);
builder.addKeyline(o4, 0.8f, size*0.9f, false, false); // throws: increased after focal

// after
builder.addKeylineRange(of0, of1, itemSize, itemSize, true, false); // focal run
builder.addKeyline(o3, 0.4f, size*0.9f, false, false);
builder.addKeyline(o4, 0.8f, size*0.5f, false, false);
Defensive patterns

Strategy: validation

Validate before calling

double prevSize = itemSize;
for (KeylineSpec k : trailingKeylines) {
  if (k.maskedItemSize >= prevSize) throw new IllegalStateException("trailing sizes must descend");
  prevSize = k.maskedItemSize;
  builder.addKeyline(k.offset, k.mask, k.maskedItemSize, false, false);
}

Prevention

When it happens

Trigger: Custom CarouselStrategy emitting trailing keylines in increasing or jumbled size order after the focal run — e.g. sizes [1.0(focal), 0.5, 0.9]. Applies only to keylines following the last isFocal=true keyline, with positive maskedItemSize.

Common situations: Same class of bug as the leading-order error: formula direction flipped, arrays reversed when porting orientation, or copy-pasted keyline blocks with the wrong sort direction on the trailing side. The builder fails on first carousel measure, pointing at the exact addKeyline call.

Related errors


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