-
-
Save belinwu/90825850c599a4d20e413c25509d1a8a to your computer and use it in GitHub Desktop.
This composable displays a horizontally scrollable week-style date selector with a 3D rotating dial effect. As the user scrolls, each date card scales, rotates, and fades based on its distance from the center, making the middle item appear highlighted. Tapping a date selects it and automatically scrolls The UI uses simple Material text styles, s…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // add this to your commonMain.dependencies | |
| // implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") | |
| @OptIn(ExperimentalTime::class) | |
| @Composable | |
| fun CarouselCalendar() { | |
| //// Clock.System.now() -> Output: 2025-11-15T14:30:45.123456789Z | |
| //// (Year-Month-Day T Hour:Minute:Second.Nanoseconds Z for UTC) | |
| //// Output: 2025-11-15T20:00:45.123456789 (if you're in IST, UTC+5:30) | |
| val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date | |
| // Output: 2025-11-15 | |
| val year = today.year // 2025 | |
| // Always start from 1st January | |
| val startDate = LocalDate(year, 1, 1) | |
| val endDate = LocalDate(year, 12, 31) | |
| // Generate full year date list | |
| val dates = remember { | |
| generateSequence(startDate) { date -> | |
| val next = date.plus(1, DateTimeUnit.DAY) | |
| if (next <= endDate) next else null | |
| }.toList() | |
| } | |
| // Selected item is today's date (even though list starts at Jan 1) | |
| var selectedDate by remember { mutableStateOf(today) } | |
| BoxWithConstraints( | |
| modifier = Modifier.fillMaxWidth() | |
| ) { | |
| DialerWeekCalendar( | |
| dates = dates, | |
| selectedDate = selectedDate, | |
| onDateSelected = { selectedDate = it }, | |
| maxWidth = maxWidth | |
| ) | |
| } | |
| } | |
| @Composable | |
| fun DialerWeekCalendar( | |
| modifier: Modifier = Modifier, | |
| dates: List<LocalDate>, | |
| selectedDate: LocalDate, | |
| onDateSelected: (LocalDate) -> Unit, | |
| maxWidth: Dp | |
| ) { | |
| val listState = rememberLazyListState() | |
| var isInitialLoad by remember { mutableStateOf(true) } | |
| // Auto-scroll to center the selected date | |
| LaunchedEffect(selectedDate) { | |
| val selectedIndex = dates.indexOf(selectedDate) | |
| if (selectedIndex != -1) { | |
| if (isInitialLoad) { | |
| listState.scrollToItem(selectedIndex) | |
| isInitialLoad = false | |
| } else { | |
| listState.animateScrollToItem(selectedIndex) | |
| } | |
| } | |
| } | |
| LazyRow( | |
| modifier = modifier | |
| .fillMaxWidth() | |
| .height(120.dp), | |
| state = listState, | |
| horizontalArrangement = Arrangement.spacedBy(8.dp), | |
| verticalAlignment = Alignment.CenterVertically, | |
| contentPadding = PaddingValues(horizontal = (maxWidth / 2) - 46.dp) | |
| ) { | |
| items(dates.size) { index -> | |
| val date = dates[index] | |
| // Center calculation | |
| val layoutInfo = listState.layoutInfo | |
| val viewportCenter = | |
| layoutInfo.viewportStartOffset + | |
| (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 | |
| val itemInfo = layoutInfo.visibleItemsInfo.find { it.index == index } | |
| val itemCenter = itemInfo?.let { it.offset + it.size / 2 } ?: 0 | |
| // Distance from center | |
| val distanceFromCenter = if (itemInfo != null) { | |
| abs(viewportCenter - itemCenter).toFloat() / layoutInfo.viewportSize.width | |
| } else 1f | |
| val scale = (1f - (distanceFromCenter * 0.3f)).coerceIn(0.7f, 1f) | |
| val rotationY = (distanceFromCenter * 40f).coerceAtMost(45f) | |
| val alpha = (1f - (distanceFromCenter * 0.5f)).coerceIn(0.5f, 1f) | |
| Box( | |
| modifier = Modifier | |
| .graphicsLayer { | |
| scaleX = scale | |
| scaleY = scale | |
| this.rotationY = | |
| if (itemCenter < viewportCenter) rotationY else -rotationY | |
| this.alpha = alpha | |
| } | |
| .clip(RoundedCornerShape(12.dp)) | |
| .background( | |
| if (date == selectedDate) Color(0xFF1E88E5) else Color.White | |
| ) | |
| .clickable { onDateSelected(date) } | |
| .width(80.dp) | |
| .height(100.dp), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Column( | |
| horizontalAlignment = Alignment.CenterHorizontally, | |
| verticalArrangement = Arrangement.Center | |
| ) { | |
| Text( | |
| text = date.month.name.take(3), | |
| style = MaterialTheme.typography.bodySmall.copy( | |
| color = if (date == selectedDate) Color.White.copy(alpha = 0.8f) | |
| else Color.DarkGray | |
| ) | |
| ) | |
| Spacer(modifier = Modifier.height(2.dp)) | |
| Text( | |
| text = date.dayOfMonth.toString(), | |
| style = MaterialTheme.typography.headlineSmall.copy( | |
| fontWeight = FontWeight.Bold, | |
| color = if (date == selectedDate) Color.White else Color.Black | |
| ) | |
| ) | |
| Spacer(modifier = Modifier.height(2.dp)) | |
| Text( | |
| text = date.dayOfWeek.name.take(3), | |
| style = MaterialTheme.typography.bodySmall.copy( | |
| color = if (date == selectedDate) Color.White.copy(alpha = 0.8f) | |
| else Color.Gray | |
| ) | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment