Blog Infos
Author
Published
Topics
, , , ,
Published

Animated Date Carousel, Demo

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)
)

Itemsize = item width(80.dp) + padding (L,R) (12.dp)= 96.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

Offset will change, based on the amount it scrolled

  • 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

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

The ever increasing convergence of native iOS and Android mobile development

This talk will illustrate with examples where we find ourselves today in the world of iOS and Android mobile development with the ever increasing convergence of the languages and frameworks we’re using, covering for example…
Watch Video

The ever increasing convergence of native iOS and Android mobile development

John O'Reilly
Android Software Engineer
Neat

The ever increasing convergence of native iOS and Android mobile development

John O'Reilly
Android Software Eng ...
Neat

The ever increasing convergence of native iOS and Android mobile development

John O'Reilly
Android Software Engineer
Neat

Jobs

Enjoy

Let’s connect and share our greatest and weirdest ideas :
LinkedIn
Github
Youtube [Hindi]

This article was previously published on proandroiddev.com

Menu