Skip to content

Latest commit

 

History

History
566 lines (427 loc) · 23.4 KB

Compose.md

File metadata and controls

566 lines (427 loc) · 23.4 KB

Calendar Compose Documentation

Table of Contents

Quick links

Check out the sample app if you have not yet done so. Most techniques that you would want to implement are already done in the examples.

Download the sample app here

Read the sample app's source code here

Add the library to your project here

If you are looking for the view-based documentation, you can find it here

Compose versions

Ensure that you are using the library version that matches with the Compose UI version in your project. If you use a version of the library that has a higher version of Compose UI than the one in your project, gradle will upgrade the Compose UI version in your project via transitive dependency.

Compose UI Calendar Library
1.2.x 2.0.x
1.3.x 2.1.x - 2.2.x
1.4.x 2.3.x
1.5.x 2.4.x
1.6.x 2.5.x

Calendar Composables

The library can be used via four composables:

HorizontalCalendar(): Horizontally scrolling month-based calendar.

VerticalCalendar(): Vertically scrolling month-based calendar.

WeekCalendar(): Horizontally scrolling week-based calendar.

HeatMapCalendar(): Horizontally scrolling heatmap calendar, useful for showing how data changes over time. A popular example is the user contribution chart on GitHub.

All composables are based on LazyRow/LazyColumn for efficiency.

In the examples below, we will mostly use the month-based HorizontalCalendar and VerticalCalendar composables since all the calendar composables share the same basic concept. If you want a week-based calendar, use the WeekCalendar composable instead. Most state properties/methods with the name prefix/suffix month (e.g firstVisibleMonth) in the month-base calendar will have an equivalent with the name prefix/suffix week (e.g firstVisibleWeek) in the week-based calendar.

Usage

HorizontalCalendar and VerticalCalendar:

@Composable
fun MainScreen() {
    val currentMonth = remember { YearMonth.now() }
    val startMonth = remember { currentMonth.minusMonths(100) } // Adjust as needed
    val endMonth = remember { currentMonth.plusMonths(100) } // Adjust as needed
    val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library

    val state = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = firstDayOfWeek
    )

    HorizontalCalendar(
        state = state,
        dayContent = { Day(it) }
    )

//    If you need a vertical calendar.
//    VerticalCalendar(
//        state = state,
//        dayContent = { Day(it) }
//    )  
}

Note: There is an additional parameter that can be provided when creating the state: outDateStyle. This determines how the out-dates are generated. See the properties section to understand this parameter.

WeekCalendar:

@Composable
fun MainScreen() {
    val currentDate = remember { LocalDate.now() }
    val currentMonth = remember { YearMonth.now() }
    val startDate = remember { currentMonth.minusMonths(100).atStartOfMonth() } // Adjust as needed
    val endDate = remember { currentMonth.plusMonths(100).atEndOfMonth() } // Adjust as needed
    val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library

    val state = rememberWeekCalendarState(
        startDate = startDate,
        endDate = endDate,
        firstVisibleWeekDate = currentDate,
        firstDayOfWeek = firstDayOfWeek
    )

    WeekCalendar(
        state = state,
        dayContent = { Day(it) }
    )
}

Your Day composable in its simplest form would be:

@Composable
fun Day(day: CalendarDay) {
    Box(
        modifier = Modifier
            .aspectRatio(1f), // This is important for square sizing!
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString())
    }
}

In the above example, we use Modifier.aspectRatio(1f), this is because the calendar assigns the width of the month divided by 7 as the width of each day cell. No height is set, so you have the flexibility to decide what works best for you. To get the typical square look on a calendar, you use Modifier.aspectRatio(1f) which tells the box to make its height the same size as the assigned width.

You can choose to set a specific height if you want. For example: Modifier.height(70.dp)

And that's all you need for simple usage! But keep reading, there's more!

First day of the week and Day of week titles.

Of course, you want to show the day of week titles on the appropriate days on the calendar.

Sun | Mon | Tue | Wed | Thu | Fri | Sat

Here's a method that generates the weekdays from the user's current Locale.

val daysOfWeek = daysOfWeek() // Available in the library

The function takes a firstDayOfWeek parameter in case you want to generate the days of week such that the desired day is at the first position.

For example:

val daysOfWeek = daysOfWeek(firstDayOfWeek = DayOfWeek.THURSDAY)
// Will produce => Thu | Fri | Sat | Sun | Mon | Tue | Wed 

Using the daysOfWeek list, you can set up the calendar so the first day of the week is what the user would expect. This could be Sunday, Monday, etc. It is good practice to use what the Locale returns as that's what the user would expect.

To set up the calendar state using the provided daysOfWeek list:

- val firstDayOfWeek = remember { firstDayOfWeekFromLocale() }
+ val daysOfWeek = remember { daysOfWeek() }
  val state = rememberCalendarState(
      startMonth = startMonth,
      endMonth = endMonth,
      firstVisibleMonth = currentMonth,
-     firstDayOfWeek = firstDayOfWeek
+     firstDayOfWeek = daysOfWeek.first()
  )

You should also use the daysOfWeek list values to set up the weekday titles, this way it matches what is shown on the calendar.

To set up the day of week titles, you can either use the month header which would show the titles on every month and allow the titles to scroll with the month, or you can show the title on a static composable above the calendar. Both ways are covered below:

Setup days of week using a static title composable:

@Composable
fun DaysOfWeekTitle(daysOfWeek: List<DayOfWeek>) {
    Row(modifier = Modifier.fillMaxWidth()) {
        for (dayOfWeek in daysOfWeek) {
            Text(
                modifier = Modifier.weight(1f),
                textAlign = TextAlign.Center,
                text = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
            )
        }
    }
}

Now you can use the title composable with the calendar in a column:

@Composable
fun MainScreen() {
    Column {
        DaysOfWeekTitle(daysOfWeek = daysOfWeek) // Use the title here
        HorizontalCalendar(
            state = state,
            dayContent = { Day(it) }
        )
    }
}

To use the titles as a month header so it scrolls with each month, continue to the month headers and footers section below!

Month headers and footers.

To add a header or footer to each month, the procedure is the same as we did for the day using the dayContent calendar parameter, but instead of the dayContent, you have to provide the monthHeader or monthFooter composable parameter.

To add the days of week titles as the month header, we can set the same DaysOfWeekTitle composable discussed above as the monthHeader parameter:

@Composable
fun MainScreen() {
    HorizontalCalendar(
        state = state,
        dayContent = { Day(it) },
        monthHeader = {
            DaysOfWeekTitle(daysOfWeek = daysOfWeek) // Use the title as month header
        }
    )
}

In the code above, we use the same daysOfWeek list that was created when we initialized the calendar state. However, we can also get the daysOfWeek list from the month data passed into the monthHeader parameter:

@Composable
fun MainScreen() {
    HorizontalCalendar(
        state = state,
        dayContent = { Day(it) },
        monthHeader = { month ->
            // You may want to use `remember {}` here so the mapping is not done 
            // every time as the days of week order will never change unless 
            // you set a new value for `firstDayOfWeek` in the state.
            val daysOfWeek = month.weekDays.first().map { it.date.dayOfWeek }
            MonthHeader(daysOfWeek = daysOfWeek)
        }
    )
}

With the day of week titles added, you can have a calendar that looks like this:

Month calendar

You can do more than just use the day titles as the header. For example, you can also show the month name if it is not already shown somewhere outside the calendar. Feel free to get creative with your month headers and footers! For complex usages, please see the sample project.

Calendar containers

Two interesting parameters of the calendar composable are the monthBody and monthContainer. You will typically not need these. But if you want to do some customizations before the calendar is rendered, then this is the place for it.

For example, if you want to draw a gradient behind the container where all the days are rendered and add rounded corners/borders to the entire month container and also shrink the entire month container so it does not fit the screen width, the monthBody and monthContainer will be:

@Composable
fun MainScreen() {
    HorizontalCalendar(
        // Draw the day content gradient.
        monthBody = { _, content ->
            Box(
                modifier = Modifier.background(
                    brush = Brush.verticalGradient(
                        colors = listOf(
                            Color(0xFFB2EBF2),
                            Color(0xFFB2B8F2)
                        )
                    )
                )
            ) {
                content() // Render the provided content!
            }
        },
        // Add the corners/borders and month width.
        monthContainer = { _, container ->
            val configuration = LocalConfiguration.current
            val screenWidth = configuration.screenWidthDp.dp
            Box(
                modifier = Modifier
                    .width(screenWidth * 0.73f)
                    .padding(8.dp)
                    .clip(shape = RoundedCornerShape(8.dp))
                    .border(
                        color = Color.Black,
                        width = 1.dp,
                        shape = RoundedCornerShape(8.dp)
                    )
            ) {
                container() // Render the provided container!
            }
        }
    )
}

With the above monthBody and monthContainer configurations, we will have this calendar:

Background styles

Other composable parameters.

  • calendarScrollPaged: The scrolling behavior of the calendar. When true, the calendar will snap to the nearest month after a scroll or swipe action. When false, the calendar scrolls normally.

  • userScrollEnabled: Whether scrolling via the user gestures or accessibility actions is allowed. You can still scroll programmatically using the state even when it is disabled. Inherited from LazyRow/LazyColumn.

  • reverseLayout: reverse the direction of scrolling and layout. When true, months will be composed from the end to the start and startMonth will be located at the end. Inherited from LazyRow/LazyColumn.

  • contentPadding: A padding around the whole calendar. This will add padding for the content after it has been clipped, which is not possible via modifier parameter. Inherited from LazyRow/LazyColumn.

State properties

All properties set when creating the state via rememberCalendarState() or rememberWeekCalendarState() can be updated in the future via the appropriate property in the state object. There are also other interesting properties in the state objects worth mentioning.

CalendarState properties for HorizontalCalendar and VerticalCalendar:

  • firstVisibleMonth: The first month that is visible on the calendar.

  • lastVisibleMonth: The last month that is visible on the calendar.

  • layoutInfo: A subclass of LazyListLayoutInfo calculated during the last layout pass. For example, you can use it to calculate what items are currently visible.

  • isScrollInProgress: Whether this calendar is currently scrolling by gesture, fling, or programmatically.

  • outDateStyle: This determines how outDates are generated for each month on the calendar. It can be one of two values:

    1. EndOfRow: The calendar will generate outDates until it reaches the end of the month row. This means that if a month has 5 rows, it will display 5 rows and if a month has 6 rows, it will display 6 rows.
    2. EndOfGrid: The calendar will generate outDates until it reaches the end of a 6 x 7 grid on each month. This means that all months will have 6 rows.

    This value can also be provided when the calendar state is initialized via rememberCalendarState(outDateStyle = ...).

If you are wondering what outDates and inDates mean, let's use the screenshot below as an example.

in-dates and out-dates

In the image, the dates within the green annotation are inDates, the ones within the red annotation are outDates while those without annotation are monthDates. You can check for this when your calendar day is rendered. To achieve the exact effect on the image, we update our Day composable:

@Composable
fun Day(day: CalendarDay) {
    Box(
        modifier = Modifier
            .aspectRatio(1f),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = day.date.dayOfMonth.toString(),
            color = if (day.position == DayPosition.MonthDate) Color.White else Color.Gray
        )
    }
}

inDates have their position property set to DayPosition.InDate

outDates have their position property set to DayPosition.OutDate

monthDates have their position property set to DayPosition.MonthDate as seen in the code snippet above.

WeekCalendarState properties for WeekCalendar:

  • firstVisibleWeek: The first week that is visible on the calendar.

  • lastVisibleWeek: The last week that is visible on the calendar.

  • layoutInfo: A subclass of LazyListLayoutInfo calculated during the last layout pass. For example, you can use it to calculate what items are currently visible.

  • isScrollInProgress: Whether this calendar is currently scrolling by gesture, fling, or programmatically.

State methods

CalendarState methods:

  • scrollToMonth(month: YearMonth): Instantly scroll to a specific month on the calendar without an animation.

  • animateScrollToMonth(month: YearMonth): Scroll to a month on the calendar with smooth scrolling animation.

WeekCalendarState methods:

  • scrollToWeek(date: LocalDate): Instantly scroll to the week containing the given date on the calendar without an animation.

  • animateScrollToWeek(date: LocalDate): Scroll to the week containing the given date on the calendar with smooth scrolling animation.

There's no need to repeat the documentation here. Please see the relevant class for all properties and methods available with proper documentation.

Date clicks

You can handle clicks in your Day composable as you would for any other composable via the modifier:

@Composable
fun Day(day: CalendarDay, onClick: (CalendarDay) -> Unit) {
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .clickable(
                enabled = day.position == DayPosition.MonthDate,
                onClick = { onClick(day) }
            ),
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString())
    }
}

Date Selection

The library has no inbuilt concept of selected/unselected dates, this gives you the freedom to choose how best you would like to implement this use case.

Implementing date selection is as simple as showing a background on a specific date in the Day composable.

For this example, I want only the last clicked date to be selected on the calendar.

Firstly, we update our Day composable to show a circle background if the date is selected:

@Composable
fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) {
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .clip(CircleShape)
            .background(color = if (isSelected) Color.Green else Color.Transparent)
            .clickable(
                enabled = day.position == DayPosition.MonthDate,
                onClick = { onClick(day) }
            ),
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString())
    }
}

Next, using the click logic already shown in the date click section above, we update the selected date state whenever a date is clicked:

@Composable
fun MainScreen() {
    var selectedDate by remember { mutableStateOf<LocalDate?>(null) }
    HorizontalCalendar(
        state = state,
        dayContent = { day ->
            Day(day, isSelected = selectedDate == day.date) { day ->
                selectedDate = if (selectedDate == day.date) null else day.date
            }
        }
    )
}

For more complex selection logic like range selection, please see the sample project. It's quite simple, the magic is all in your logic!

Disabling dates

As expected, the library does not provide this logic internally so you have complete flexibility.

To disable dates, you can simply set the texts on those dates to look disabled and disable clicks on those dates. For example, if we want to show in and out dates but disable them so that they cannot be selected, we can just set a different color on the texts.

We actually already did this with the example in the date click section, we already ignore clicks for in and out dates using this logic:

@Composable
fun Day(day: CalendarDay, onClick: (CalendarDay) -> Unit) {
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .clickable(
                enabled = day.position == DayPosition.MonthDate, // Only month-dates are clickable.
                onClick = { onClick(day) }
            ),
        contentAlignment = Alignment.Center
    ) { // Change the color of in-dates and out-dates, you can also hide them completely!
        Text(
            text = day.date.dayOfMonth.toString(),
            color = if (day.position == DayPosition.MonthDate) Color.White else Color.Gray
        )
    }
}

And we've now covered the typical usage. The beauty of the library is in its limitless possibilities. You are not constrained on how to build your user interface, the library provides you with the needed calendar data logic and you provide the desired UI logic.

See the sample project for some complex implementations.

Week calendar

As discussed previously, the library provides HorizontalCalendar, VerticalCalendar, and WeekCalendar composables. The WeekCalendar is a week-based calendar. Almost all topics covered above for the month calendar will apply to the week calendar. The main difference is that state properties/methods will have a slightly different name, typically with a week prefix/suffix instead of month.

For example: firstVisibleMonth => firstVisibleWeek, scrollToMonth() => scrollToWeek() and many others, but you get the idea.

We already showed how to use the WeekCalendar previously in the usage section, but in the most basic form, it is:

@Composable
fun MainScreen() {
    val state = rememberWeekCalendarState(
        startDate = ...,
        endDate = ...,
        firstVisibleWeekDate = ...,
        firstDayOfWeek = ...
    )
    WeekCalendar(
        state = state,
        dayContent = { Day(it) }
    )  
}

A week calendar implementation from the sample app:

Week calendar

If you would like to toggle the calendar between month and week modes, please see the sample app where we did this by animating the Modifier height and alternatively using the AnimatedVisibility API.

HeatMap calendar

This is a horizontally scrolling heatmap calendar implementation, useful for showing how data changes over time. A popular example is the user contribution chart on GitHub. Another usage could be to show changes in the frequency of a habit tracked by a user.

A screenshot from the sample app is shown below:

HeatMap calendar

All the properties in the month-based calendar are also available in the HeatMap calendar except OutDateStyle configuration as that is not relevant in this case. Note that there are out-dates on the calendar but since the dates are laid out in columns instead of rows, the two OutDateStyle options EndOfRow and EndOfGrid are not needed here. All other month-based properties are available!

Basic HeatMap calendar usage:

@Composable
fun MainScreen() {
    val currentMonth = remember { YearMonth.now() }
    val startMonth = remember { currentMonth.minusMonths(100) } // Adjust as needed
    val endMonth = remember { currentMonth.plusMonths(100) } // Adjust as needed
    val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library

    val state = rememberHeatMapCalendarState(
        startMonth = startMonth,
        endMonth = startMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = firstDayOfWeek,
    )
    HeatMapCalendar(
        state = state,
        dayContent = { day, _ -> Day(day) },
        weekHeader = { WeekHeader(it) },
        monthHeader = { MonthHeader(it) }
    )
}

Please see the HeatMapCalendar composable for the full documentation. There are also examples in the sample app.

Remember that all the screenshots shown so far are just examples of what you can achieve with the library and you can absolutely build your calendar to look however you want.

Made a cool calendar with this library? Share an image here.