iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📆

[Android] Trying out the Calendar library

に公開

Calendar

A highly customizable calendar library for Android, utilizing RecyclerView for the View system and LazyRow/LazyColumn for Compose.

This is a note from when I briefly tried out the Calendar library, which is useful for handling calendars within Android apps.

Verification Environment

$ sw_vers
ProductName: macOS
ProductVersion: 13.4
BuildVersion: 22F66

Android Studio Giraffe | 2022.3.1 Patch 1

Features of the Calendar Library

  • Single, multiple, and range selection - perform date selection using your preferred method
  • Week or month mode - display week-based calendars or standard monthly calendars
  • Disable specific dates to make them unselectable
  • Restrict the calendar's date range
  • Custom date view/composable
  • Custom calendar view/composable
  • Use any day as the first day of the week
  • Horizontal or vertical scrolling calendars

Package Installation

As described here, you specify the Calendar version according to the Compose UI version.

Since I'm using compose bom version 2022.10.00 this time, referring here shows that the Compose UI version is 1.3.0, so I'll use 2.2.0.

Add the following to libs.versions.toml:

[versions]
calendar-view = "2.2.0"

[libraries]
calendar-view = { module = "com.kizitonwose.calendar:compose", version.ref = "calendar-view" }

Add the following to app/build.gradle.kts:

dependencies {
  implementation(libs.calendar.view)
}

Documentation

Calendar/docs/Compose.md at main · kizitonwose/Calendar

I will proceed by referring to the link above.

Four Main Composables

  1. HorizontalCalendar
    • Monthly calendar with horizontal scrolling
  2. VerticalCalendar
    • Monthly calendar with vertical scrolling
  3. WeekCalendar
    • Weekly calendar with horizontal scrolling
  4. HeatMapCalendar
    • Like the contribution graph on GitHub

First, let's try a minimal implementation with HorizontalCalendar

@Composable
fun CalendarCompose() {
    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
    )

    Column(modifier = Modifier.fillMaxSize()) {
        HorizontalCalendar(
            state = state,
            dayContent = { Day(it) }
        )
    }
}

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

The following shows how it looks when running on an emulator.

image1.gif

Behavior of VerticalCalendar, WeekCalendar, and HeatMapCalendar

Next, I'll try using each type of calendar.

  • VerticalCalendar
    Just changing HorizontalCalendar to VerticalCalendar looks like this:
    image2.gif

  • WeekCalendar

    @Composable
    fun WeekCalendarSimple() {
        val currentDate = remember { LocalDate.now() }
        val currentMonth = remember { YearMonth.now() }
        val startDate = remember { currentMonth.atStartOfMonth() } // Adjust as needed
        val endDate = remember { currentMonth.atEndOfMonth() } // Adjust as needed
        val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library
    
        val state = rememberWeekCalendarState(
            startDate = startDate,
            endDate = endDate,
            firstVisibleWeekDate = currentDate,
            firstDayOfWeek = firstDayOfWeek
        )
    
        Column(modifier = Modifier.fillMaxSize()) {
            WeekCalendar(
                state = state,
                dayContent = { Day(it) }
            )
        }
    }
    
    @Composable
    private fun Day(day: WeekDay) {
        Box(
            modifier = Modifier
                .aspectRatio(1f), // This is important for square sizing!
            contentAlignment = Alignment.Center
        ) {
            Text(text = day.date.dayOfMonth.toString())
        }
    }
    

    image3.gif

  • HeatMapCalendar

    @Composable
    fun HeatMapCalendarSimple() {
        val currentMonth = remember { YearMonth.now() }
        val endDate = remember { LocalDate.now() }
        val startDate = remember { endDate.minusMonths(12) }
    
        val state = rememberHeatMapCalendarState(
            startMonth = startDate.yearMonth,
            endMonth = endDate.yearMonth,
            firstVisibleMonth = endDate.yearMonth,
            firstDayOfWeek = firstDayOfWeekFromLocale(),
        )
    
        Column(modifier = Modifier.fillMaxSize()) {
            HeatMapCalendar(
                state = state,
                dayContent = { day, week -> Day(day, startDate, endDate, week) }
            )
        }
    }
    
    @Composable
    private fun Day(
        day: CalendarDay,
        startDate: LocalDate,
        endDate: LocalDate,
        week: HeatMapWeek,
    ) {
        val weekDates = week.days.map { it.date }
        if (day.date in startDate..endDate) {
            LevelBox(Color.Green)
        } else if (weekDates.contains(startDate)) {
            LevelBox(Color.Transparent)
        }
    }
    
    @Composable
    private fun LevelBox(color: Color) {
        Box(
            modifier = Modifier
                .size(18.dp) // Must set a size on the day.
                .padding(2.dp)
                .clip(RoundedCornerShape(2.dp))
                .background(color = color)
        )
    }
    

    image4.gif

Adding a WeekTitle

The documentation can be found here.

@Composable
fun HorizontalCalendarWeekTitle() {
    val currentMonth = remember { YearMonth.now() }
    val startMonth = remember { currentMonth.minusMonths(100) } // Adjust as needed
    val endMonth = remember { currentMonth.plusMonths(100) } // Adjust as needed
    val daysOfWeek = remember { daysOfWeek() }

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

    Column(modifier = Modifier.fillMaxSize()) {
        DaysOfWeekTitle(daysOfWeek = daysOfWeek)
        HorizontalCalendar(
            state = state,
            dayContent = { Day(it) }
        )
    }
}

@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()),
            )
        }
    }
}

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

The following shows how it looks when running on an emulator.

image5.gif

Thoughts

Although I've only tried it briefly, I feel that this is a highly customizable library, just as the documentation says, based on a simple base calendar.
I'll write another article when I dive deeper into it.

Discussion