material-components/material-components-android · error · IllegalStateException

All children of a RecyclerView using CarouselLayoutManager m

Error message

All children of a RecyclerView using CarouselLayoutManager must use MaskableFrameLayout as their root ViewGroup.

What it means

CarouselLayoutManager implements its item masking by casting each RecyclerView child to the Maskable interface (implemented by MaskableFrameLayout) inside measureChildWithMargins (CarouselLayoutManager.java:1028). Any child that is not Maskable makes per-child masking impossible, so the layout manager throws IllegalStateException — the carousel contract requires MaskableFrameLayout as the item root.

Source

Thrown at lib/java/com/google/android/material/carousel/CarouselLayoutManager.java:1028

        new RectF(getParentLeft(), getParentTop(), getParentRight(), getParentBottom());
    // If the carousel is a CONTAINED carousel, ensure the mask collapses against the side of the
    // container instead of bleeding and being clipped by the RecyclerView's bounds.
    // Only do this if there is only one side of the mask that is out of bounds; if
    // both sides are out of bounds on the same side, then the whole mask is out of view.
    if (carouselStrategy.getStrategyType() == StrategyType.CONTAINED) {
      orientationHelper.containMaskWithinBounds(maskRect, offsetMaskRect, parentBoundsRect);
    }

    // 'Push out' any masks that are on the parent edge by rounding up/down and adding or
    // subtracting a pixel. Otherwise, the mask on the 'edge' looks like it has a width of 1 pixel.
    orientationHelper.moveMaskOnEdgeOutsideBounds(maskRect, offsetMaskRect, parentBoundsRect);
    ((Maskable) child).setMaskRectF(maskRect);
  }

  @Override
  public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
    if (!(child instanceof Maskable)) {
      throw new IllegalStateException(
          "All children of a RecyclerView using CarouselLayoutManager must use MaskableFrameLayout"
              + " as their root ViewGroup.");
    }

    LayoutParams lp = (LayoutParams) child.getLayoutParams();

    Rect insets = new Rect();
    calculateItemDecorationsForChild(child, insets);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;

    // If the strategy's keyline set is available, use the item size from the keyline set.
    // Otherwise, measure the item to what it would like to be so the strategy will be given an
    // opportunity to use this desired size in making it's sizing decision.
    final float childWidthDimension =
        keylineStateList != null && orientationHelper.orientation == HORIZONTAL
            ? keylineStateList.getDefaultState().getItemSize()
            : lp.width;

View on GitHub (pinned to ac7e18efee)

Solutions

  1. Make the item layout root <com.google.android.material.carousel.MaskableFrameLayout> and put the real item content in a child of it.
  2. If a custom root view class is needed, extend MaskableFrameLayout so the Maskable cast still succeeds.
  3. Verify every view type in the adapter (headers/footers included) uses a Maskable root — a single non-maskable view type crashes the whole layout pass.

Example fix

<!-- before -->
<androidx.constraintlayout.widget.ConstraintLayout ... />

<!-- after -->
<com.google.android.material.carousel.MaskableFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <androidx.constraintlayout.widget.ConstraintLayout ...>...</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.carousel.MaskableFrameLayout>
Defensive patterns

Strategy: type-guard

Validate before calling

if (view instanceof Maskable) {
  recyclerView.setLayoutManager(new CarouselLayoutManager());
} // and ensure every item view root is MaskableFrameLayout before binding

Type guard

static boolean isCarouselItemValid(View itemView) {
  return itemView instanceof Maskable;
}

Prevention

When it happens

Trigger: Supplying a RecyclerView.Adapter whose item layout root is not MaskableFrameLayout (e.g. a ConstraintLayout, MaterialCardView or FrameLayout root, or a fragment-created view) to a RecyclerView using CarouselLayoutManager. The throw happens during measurement of the first child, i.e. at first layout pass.

Common situations: Building a carousel with an existing list item layout instead of the catalog sample layout. Copying the demo adapter but replacing the item XML root with ConstraintLayout 'for convenience'. Also hitting it after refactoring item views into custom views that no longer extend MaskableFrameLayout.

Related errors


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