Blog Infos

This UI was Inspired by a Figma design
Hey Everyone,
Today we are going to learn how to build this beautiful Compose Animated Date Carousel for Android and IOS, Built using Jetpack compose and kotlinx-datetime , and Curiosity 🙂 .
Supported platform — Compose Multiplatform

→ Get the Current Time
val currentTime = Clock.System.now()
// Output: 2025-11-15T14:30:45.123456789Z
val today = currentTime.toLocalDateTime(TimeZone.currentSystemDefault()).date
// Output: 2025-11-17
- Set the start and End Dates
// Start from 1st January and ends 31st jan
val startDate = LocalDate(year, 1, 1)
val endDate = LocalDate(year, 12, 31)
- Generate Full year date list
/** generateSequence takes a genric type,
* we are passing it a type of LocalDate
* and adding each day, until it reaches the last date
*/
val dates = remember {
generateSequence(startDate) { date ->
val next = date.plus(1, DateTimeUnit.DAY)
if (next <= endDate) next else null
}.toList()
}
This will generate and give the list of all the dates from Jan 1st to Dec 31st, thus we have completed the step 1 here.

→ Parent
// Taking BoxWithContstraint, in order to get the screenWidth using `maxWidth`
BoxWithConstraints(
modifier = Modifier.fillMaxWidth()
) {
DialerWeekCalendar(
dates = dates,
selectedDate = selectedDate,
onDateSelected = { selectedDate = it },
maxWidth = maxWidth
)
}
→ DialerWeekCalendar
/** This will Auto-scroll to center the selected date
* When the screen first loads, `scrollToItem` is used which scrolls -
* without animating, thus didn't takes time to scroll and reach
* `animateScrollToItem` animates when you click on any date to center
*/
LaunchedEffect(selectedDate) {
val selectedIndex = dates.indexOf(selectedDate)
if (selectedIndex != -1) {
if (isInitialLoad) {
listState.scrollToItem(index = selectedIndex)
isInitialLoad = false
} else {
// Animate scroll for subsequent selections
listState.animateScrollToItem(index = selectedIndex)
}
}
}
- Lazy Row
/** content padding is used here to align the first Date (1st Jan)
* and 31st Dec, in the center, as there are no elements before and after
* it, respectively, giving it the
* `center - half the width of item (46.dp)`
*/
LazyRow(
modifier = modifier
.fillMaxWidth()
.height(Spacing.s30),
state = listState,
horizontalArrangement = Arrangement.spacedBy(Spacing.s3),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(horizontal = (maxWidth / 2) - 46.dp)
)

- Items loop
/**
* We loop through the dates list using the index.
* Each index gives one date that we will render inside the LazyRow.
*/
items(dates.size) { index ->
val date = dates[index]
- Get layout info and viewport center
/**
* Get the layout information from the LazyListState.
* From this, calculate the center of the visible area.
* This center point is used to decide how much the item should scale,
* rotate, or fade based on its distance from this center.
*/
val layoutInfo = listState.layoutInfo
val viewportCenter = layoutInfo.viewportStartOffset +
(layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2

- Find item info and item centre
/**
* Try to find the item information for this index.
* If the item is visible, we can read its pixel offset and size.
* From that, we calculate the item center.
* If the item is not visible, itemInfo is null.
*/
val itemInfo = layoutInfo.visibleItemsInfo.find { it.index == index }
val itemCenter = itemInfo?.let { it.offset + it.size / 2 } ?: 0
- Distance from center
/**
* Measure how far this item is from the center of the viewport.
* Convert it into a 0 to 1 range by dividing with viewport width.
* If the item is not visible, treat the distance as 1.
*/
val distanceFromCenter = if (itemInfo != null) {
abs(viewportCenter - itemCenter).toFloat() / layoutInfo.viewportSize.width
} else {
1f
}
- Visual scale, rotation, and alpha
/**
* Based on the distance value, compute scale, rotation, and transparency.
* Items closer to the center get bigger, rotate less, and stay more visible.
* Items farther from the center shrink, rotate more, and fade out.
*/
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 with graphicsLayer effects
/**
* The Box represents each date card.
* We apply scale, rotation, and alpha using graphicsLayer.
* Rotation direction changes based on which side of the center it is on.
* The card changes background when it is the selected date.
* Width and height create the pill shaped card.
*/
Box(
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
this.rotationY =
if (itemCenter < viewportCenter) rotationY else -rotationY
this.alpha = alpha
}
.clip(RoundedCornerShape(Spacing.s4))
.background(
if (date == selectedDate) TodoColors.Primary.color
else TodoColors.Light.color
)
.clickable {
onDateSelected(date)
}
.width(Spacing.s20)
.height(Spacing.s25),
contentAlignment = Alignment.Center
) {}
- Column with month, day, and weekday
/**
* Display the month (short form), day number, and weekday (short form).
* Colors change when the date is selected.
*/
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = date.month.name.take(3),
style = BodySmall().copy(
color = if (date == selectedDate)
Color.White.copy(alpha = 0.8f)
else Color.DarkGray
)
)
VSpacer(Spacing.s1)
Text(
text = date.day.toString(),
style = H3TextStyle().copy(
fontWeight = FontWeight.Bold,
color = if (date == selectedDate) Color.White else Color.Black
),
)
VSpacer(Spacing.s1)
Text(
text = date.dayOfWeek.name.take(3),
style = BodySmall().copy(
color = if (date == selectedDate)
Color.White.copy(alpha = 0.8f)
else Color.Gray
),
)
}
That’s All,
Explore the Full Code here : Gist




