{"record":{"id":"0065100118a073b1","repo":"material-components/material-components-android","slug":"keylines-that-are-marked-as-focal-must-all-have-th","errorCode":null,"errorMessage":"Keylines that are marked as focal must all have the same masked item size.","messagePattern":"Keylines that are marked as focal must all have the same masked item size\\.","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"lib/java/com/google/android/material/carousel/KeylineState.java","lineNumber":409,"sourceCode":"        latestAnchorKeylineIndex = tmpKeylines.size();\n      }\n\n      Keyline tmpKeyline =\n          new Keyline(UNKNOWN_LOC, offsetLoc, mask, maskedItemSize, isAnchor, cutoff,\n              leftOrTopPaddingShift, rightOrBottomPaddingShift);\n      if (isFocal) {\n        if (tmpFirstFocalKeyline == null) {\n          tmpFirstFocalKeyline = tmpKeyline;\n          firstFocalKeylineIndex = tmpKeylines.size();\n        }\n\n        if (lastFocalKeylineIndex != NO_INDEX && tmpKeylines.size() - lastFocalKeylineIndex > 1) {\n          throw new IllegalArgumentException(\n              \"Keylines marked as focal must be placed next to each other. There cannot be\"\n                  + \" non-focal keylines between focal keylines.\");\n        }\n        if (maskedItemSize != tmpFirstFocalKeyline.maskedItemSize) {\n          throw new IllegalArgumentException(\n              \"Keylines that are marked as focal must all have the same masked item size.\");\n        }\n        tmpLastFocalKeyline = tmpKeyline;\n        lastFocalKeylineIndex = tmpKeylines.size();\n      } else {\n        if (tmpFirstFocalKeyline == null && tmpKeyline.maskedItemSize < lastKeylineMaskedSize) {\n          throw new IllegalArgumentException(\n              \"Keylines before the first focal keyline must be ordered by incrementing masked item\"\n                  + \" size.\");\n        } else if (tmpLastFocalKeyline != null\n            && tmpKeyline.maskedItemSize > lastKeylineMaskedSize) {\n          throw new IllegalArgumentException(\n              \"Keylines after the last focal keyline must be ordered by decreasing masked item\"\n                  + \" size.\");\n        }\n      }\n      lastKeylineMaskedSize = tmpKeyline.maskedItemSize;\n      tmpKeylines.add(tmpKeyline);","sourceCodeStart":391,"sourceCodeEnd":427,"githubUrl":"https://github.com/material-components/material-components-android/blob/ac7e18efeefb331850c561faf9ab8bf81d27ba68/lib/java/com/google/android/material/carousel/KeylineState.java#L391-L427","documentation":"Every focal keyline in a KeylineState must share the same masked item size (KeylineState.java:409): when a focal keyline's maskedItemSize differs from the first focal keyline's (tmpFirstFocalKeyline.maskedItemSize), the builder throws. Focal items are the unmasked, full-size items; a focal run with mixed sizes would make 'focal' meaningless for scroll-snap and focal-range calculations.","triggerScenarios":"Custom CarouselStrategy marking keylines with different masked sizes (e.g. a large 300dp item and a medium 240dp item) as isFocal=true in the same builder. Any per-index size variation combined with isFocal=true triggers it as soon as the second, differently-sized focal keyline is added.","commonSituations":"Strategies that vary item size across the center of the carousel (hero-style layouts) but forget that only one size may be focal — in HeroCarouselStrategy-like layouts the large item is the focal and other sizes must not be flagged focal. Arithmetic bugs producing slightly different masked sizes (float rounding on different offsets) can also cause inequality.","solutions":["Flag exactly one masked size as focal; keylines with other sizes must be non-focal.","If sizes are computed, derive them from the same itemSize constant so float equality holds (maskedItemSize for focal keylines should equal the unmasked item size).","Check a hero-style strategy (HeroCarouselStrategy) for the correct pattern when you need varied center sizes."],"exampleFix":"// before\nbuilder.addKeyline(o1, 0f, itemSize, true, false);\nbuilder.addKeyline(o2, 0f, itemSize * 0.8f, true, false); // throws: focal sizes differ\n\n// after\nbuilder.addKeyline(o1, 0f, itemSize, true, false);\nbuilder.addKeyline(o2, 0.2f, itemSize * 0.8f, false, false); // non-focal, different size OK","handlingStrategy":"validation","validationCode":"boolean focal = Math.abs(maskedItemSize - itemSize) < 0.01f; // single focal size only\nbuilder.addKeyline(offset, mask, maskedItemSize, focal, anchor);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only one masked size may be focal — derive all focal sizes from the same itemSize constant.","In hero-style layouts, flag only the large size as focal.","Compute sizes from one source constant to avoid float-equality surprises."],"tags":["android","material-components","carousel","keylines","custom-strategy","builder"],"backgroundTag":null,"analyzedSha":"ac7e18efeefb331850c561faf9ab8bf81d27ba68","analyzedAt":"2026-08-14T15:35:35.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}