A Production-Ready iOS-Style 3D Wheel Picker for Jetpack Compose
Android lacks a native cylindrical picker that matches the fluid feel of iOS. This component closes that gap with a single-file, controlled-state implementation that slots directly into Compose forms, date pickers, and cascading selectors without pulling in a third-party library.
The component maps a flat list onto a 3D cylinder using sine projection, so options arc toward the user instead of sitting in a flat column. It pairs a VerticalPager with exponential decay fling behavior to produce the characteristic fast-then-slow inertial scroll, snapping precisely to the center item when motion stops. Cyclic scrolling runs on a virtual index range near Int.MAX_VALUE, avoiding data duplication while silently re-centering to prevent boundary overflows during long sessions.
A dual-layer drawing system with mutually exclusive clipping changes text color progressively as it enters the central mask, eliminating the ghosting that occurs when simply overlaying two text styles. The component recalculates its own height from the projected visual bounds of every visible row, so the container fits the curved surface without top or bottom gaps. A controlled-state API keeps selection logic in the caller, and a bundled DateWheelPicker handles leap years, month-length transitions, and illegal-date convergence from a single LocalDate.
Every parameter — row height, visible count, curvature, scale, alpha, friction, and fling distance — is exposed through a single style object with runtime validation. The implementation stays efficient: the pager assembles only viewport-plus-buffer pages, and the dual text draw is limited to visible items.
Using VerticalPager instead of LazyColumn is the architectural linchpin — it supplies both snap behavior and off-screen page retention, two capabilities that are otherwise painful to retrofit onto a lazy list.
The dual-layer clipping approach solves a subtle rendering problem (text ghosting from overlaid styles) that many picker implementations ignore or patch with opacity hacks.
Re-centering the virtual index during long scroll sessions is a defensive detail that most cyclic-picker implementations omit, and it prevents a real crash vector when the pager state approaches Int boundaries.
Computing container height from projected bounds rather than a flat formula means the component visually self-adjusts when curvature, scale, or visible count change — a quality-of-life detail that eliminates manual tuning.