{"record":{"id":"12d60d36bb81c684","repo":"material-components/material-components-android","slug":"keylines-before-the-first-focal-keyline-must-be-or","errorCode":null,"errorMessage":"Keylines before the first focal keyline must be ordered by incrementing masked item size.","messagePattern":"Keylines before the first focal keyline must be ordered by incrementing masked item size\\.","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"lib/java/com/google/android/material/carousel/KeylineState.java","lineNumber":416,"sourceCode":"        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);\n      return this;\n    }\n\n    /**\n     * Adds a keyline along the scrolling axis where an object should be masked by the given {@code\n     * mask} and positioned at {@code offsetLoc}.\n     *","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/material-components/material-components-android/blob/ac7e18efeefb331850c561faf9ab8bf81d27ba68/lib/java/com/google/android/material/carousel/KeylineState.java#L398-L434","documentation":"Before the first focal keyline, keylines must grow monotonically in masked size as they approach the focal run (KeylineState.java:416). The builder tracks lastKeylineMaskedSize; when no focal keyline has been added yet (tmpFirstFocalKeyline == null) and the new non-focal keyline's maskedItemSize is smaller than the previous one, it throws. This ordering is what produces the carousel's characteristic 'items grow toward the center' masking curve.","triggerScenarios":"Custom CarouselStrategy emitting leading keylines in decreasing or jumbled size order — e.g. sizes [0.9, 0.5, 0.7] before the first focal keyline. The check applies only to keylines added before any isFocal=true keyline, and only for positive maskedItemSize (size <= 0F entries are skipped earlier).","commonSituations":"Loop-generated keylines whose size formula decreases with index when it should increase, or shuffled/copy-pasted keyline arrays. Also migrating a strategy between horizontal and vertical and flipping the iteration direction without flipping the size order.","solutions":["Order leading keylines ascending in maskedItemSize (smallest first, each >= the previous) up to the focal size.","If sizes come from an array/list, sort or reverse it so it ascends before the focal run.","Add a debug assertion in the strategy that logs each pre-focal size and fails fast on non-increasing sequences."],"exampleFix":"// before\nbuilder.addKeyline(o0, 0.9f, size*0.3f, false, false);\nbuilder.addKeyline(o1, 0.7f, size*0.2f, false, false); // throws: decreased\nbuilder.addKeyline(o2, 0.5f, size*0.6f, false, false);\n\n// after\nbuilder.addKeyline(o0, 0.9f, size*0.2f, false, false);\nbuilder.addKeyline(o1, 0.7f, size*0.3f, false, false);\nbuilder.addKeyline(o2, 0.5f, size*0.6f, false, false);","handlingStrategy":"validation","validationCode":"double prevSize = 0;\nfor (KeylineSpec k : leadingKeylines) {\n  if (k.maskedItemSize <= prevSize) throw new IllegalStateException(\"leading sizes must ascend\");\n  prevSize = k.maskedItemSize;\n  builder.addKeyline(k.offset, k.mask, k.maskedItemSize, false, false);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Emit leading keylines in ascending masked size.","Sort or reverse size arrays when flipping orientation/direction.","Add strategy unit tests asserting monotonic ordering."],"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"}