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

Keylines marked as focal must be placed next to each other.

Error message

Keylines marked as focal must be placed next to each other. There cannot be non-focal keylines between focal keylines.

What it means

Focal keylines are the contiguous run of full-size items at the center of a carousel. The builder enforces contiguity (KeylineState.java:404): when adding a focal keyline, if the previous focal keyline (lastFocalKeylineIndex) is more than one position back — i.e. a non-focal keyline was added between two focal ones — it throws IllegalArgumentException. The masking math and focal-range scroll calculations assume focal items form one unbroken block.

Source

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

        }
        if (latestAnchorKeylineIndex != NO_INDEX && latestAnchorKeylineIndex != 0) {
          throw new IllegalArgumentException(
              "Anchor keylines must be either the first or last keyline.");
        }
        latestAnchorKeylineIndex = tmpKeylines.size();
      }

      Keyline tmpKeyline =
          new Keyline(UNKNOWN_LOC, offsetLoc, mask, maskedItemSize, isAnchor, cutoff,
              leftOrTopPaddingShift, rightOrBottomPaddingShift);
      if (isFocal) {
        if (tmpFirstFocalKeyline == null) {
          tmpFirstFocalKeyline = tmpKeyline;
          firstFocalKeylineIndex = tmpKeylines.size();
        }

        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"

View on GitHub (pinned to ac7e18efee)

Solutions

  1. Emit all focal keylines consecutively — use addKeylineRange for the focal run — and keep small/anchor keylines strictly before or after it.
  2. Fix the focal predicate in the strategy loop so it yields a single contiguous true-run.
  3. Model the sequence on MultiBrowseCarouselStrategy: small leading keylines, one contiguous focal range, small trailing keylines.

Example fix

// before
builder.addKeyline(o, 0f, itemSize, /* focal */ true, false);
builder.addKeyline(o2, 0.5f, halfSize, /* focal */ false, false);
builder.addKeyline(o3, 0f, itemSize, /* focal */ true, false); // throws: gap between focals

// after
builder.addKeyline(o2, 0.5f, halfSize, false, false);
builder.addKeylineRange(o, o3, itemSize, itemSize, /* focal */ true); // contiguous focal run
Defensive patterns

Strategy: validation

Validate before calling

boolean focal = size == itemSize && index >= firstFocalIndex && index <= lastFocalIndex; // contiguous run only
builder.addKeyline(offset, mask, size, focal, anchor);

Prevention

When it happens

Trigger: Custom CarouselStrategy builder sequences like focal, small, focal — e.g. addKeyline(..., isFocal=true), then a non-focal keyline, then isFocal=true again. The check is tmpKeylines.size() - lastFocalKeylineIndex > 1 at the moment the later focal keyline is added.

Common situations: Generating keylines in a loop where the isFocal condition is computed per index and accidentally toggles (e.g. focal = index % 2 == 0), or inserting an extra decorative small keyline in the middle of what should be one focal run. The crash is at build time inside the strategy, so it fails on first carousel layout.

Related errors


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