kizitonwose/calendar

repository·main·Indexed 26 days ago

https://github.com/kizitonwose/calendar

A highly customizable calendar library for Android (View system), Android (Compose), and Compose Multiplatform (Android, iOS, js, WasmJs, and Desktop). It provides core calendar logic and various composables including HorizontalCalendar, VerticalCalendar, WeekCalendar, HeatMapCalendar, HorizontalYearCalendar, and VerticalYearCalendar. The library supports java.time for Android and kotlinx-datetime for Multiplatform projects.

Tokens
9.8K
Snippets
22
Records
37
Agent score
91%

What's inside kizitonwose-calendar

  1. Available Calendar Composables

    main

    The library provides six different composables for various calendar layouts. All composables are built on top of LazyRow or LazyColumn for efficient scrolling.

    • HorizontalCalendar(): A horizontally scrolling month-based calendar.
    • VerticalCalendar(): A vertically scrolling month-based calendar.
    • WeekCalendar(): A horizontally scrolling week-based calendar.
    • HeatMapCalendar(): A horizontally scrolling heatmap calendar (e.g., for GitHub-style contribution charts).
    • HorizontalYearCalendar(): A horizontally scrolling year-based calendar.
    • VerticalYearCalendar(): A vertically scrolling year-based calendar.

    Note: Most state properties and methods follow a naming convention where month (e.g., firstVisibleMonth) in month-based calendars corresponds to week (e.g., firstVisibleWeek) in week-based calendars and year (e.g., firstVisibleYear) in year-based calendars.

  2. Choose the appropriate Calendar View class

    main

    The library provides three main classes depending on the desired calendar layout. All three classes extend RecyclerView, allowing you to use standard RecyclerView customizations like decorators.

    • CalendarView: A typical month-based calendar.
    • WeekCalendarView: A week-based calendar.
    • YearCalendarView: A year-based calendar.

    Note that most XML attributes and class properties/methods use prefixes or suffixes corresponding to the view type (e.g., monthHeaderResource for CalendarView, weekHeaderResource for WeekCalendarView, and yearHeaderResource for YearCalendarView).

  3. Implement a Year view using YearCalendarView

    main

    The YearCalendarView class provides a year-based calendar implementation. Similar to the month and week views, it uses properties and XML attributes with a year prefix or suffix (e.g., yearHeaderResource, scrollToYear(), findFirstVisibleYear()).

    To display a year calendar, add the view to your XML layout and provide a day resource using app:cv_dayViewResource.

    <com.kizitonwose.calendar.view.YearCalendarView
        android:id="@+id/yearCalendarView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:cv_dayViewResource="@layout/calendar_day_layout" />
  4. Implement CalendarView (Month View)

    main

    To use the standard month-based CalendarView, follow these steps:

    1. Add to XML: Include com.kizitonwose.calendar.view.CalendarView and provide a day view resource via app:cv_dayViewResource.
    2. Create a DayViewContainer: Extend ViewContainer to hold references to your day layout views.
    3. Implement MonthDayBinder: Provide a binder to create and bind your containers.
    4. Setup the View: Call .setup() with a start month, end month, and the first day of the week.
    <com.kizitonwose.calendar.view.CalendarView
        android:id="@+id/calendarView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cv_dayViewResource="@layout/calendar_day_layout" />
    class DayViewContainer(view: View) : ViewContainer(view) {
        val textView = view.findViewById<TextView>(R.id.calendarDayText)
    }
    
    calendarView.dayBinder = object : MonthDayBinder<DayViewContainer> {
        override fun create(view: View) = DayViewContainer(view)
        override fun bind(container: DayViewContainer, data: CalendarDay) {
            container.textView.text = data.date.dayOfMonth.toString()
        }
    }
    
    val currentMonth = YearMonth.now()
    val startMonth = currentMonth.minusMonths(100)
    val endMonth = currentMonth.plusMonths(100)
    val firstDayOfWeek = firstDayOfWeekFromLocale()
    calendarView.setup(startMonth, endMonth, firstDayOfWeek)
    calendarView.scrollToMonth(currentMonth)
  5. Implement a Week view using WeekCalendarView

    main

    The WeekCalendarView class provides a week-based calendar implementation. It follows a similar setup pattern to the month calendar, but uses properties and XML attributes with a week prefix or suffix (e.g., weekHeaderResource, scrollToWeek(), findFirstVisibleWeek()).

    To display a week calendar, add the view to your XML layout and provide a day resource using app:cv_dayViewResource.

    Customizing Day Count and Size:

    • To show more or less than 7 days at a time, set app:scrollPaged="false".
    • To define custom sizes for day cells, set the daySize property to FreeForm.
    <com.kizitonwose.calendar.view.WeekCalendarView
        android:id="@+id/weekCalendarView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cv_dayViewResource="@layout/calendar_day_layout" />
  6. Migrate from 0.3.x to 0.4.x or 1.x.x

    main

    When upgrading from 0.3.x to 0.4.x or 1.x.x, the library has transitioned from ThreeTenABP to Java 8 API desugaring for date handling.

    Required Actions:

    1. Update Imports: Change all date/time related class imports from org.threeten.bp.* to java.time.*.
    2. Cleanup Initialization: Remove AndroidThreeTen.init(this) from your application class's onCreate() method, as it is no longer required.
  7. Add Month Headers and Footers

    main

    To add a header or footer to each month (e.g., scrolling month names or weekday titles), use the monthHeaderResource or monthFooterResource XML attributes and implement a MonthHeaderFooterBinder.

    Note: The header/footer view is reused, so you may want to use a tag to avoid re-binding static content every time a month is reused.

    <com.kizitonwose.calendar.view.CalendarView
        android:id="@+id/calendarView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cv_dayViewResource="@layout/calendar_day_layout"
        app:cv_monthHeaderResource="@layout/calendar_day_titles_container" />
    class MonthViewContainer(view: View) : ViewContainer(view) {
        val titlesContainer = view as ViewGroup 
    }
    
    calendarView.monthHeaderBinder = object : MonthHeaderFooterBinder<MonthViewContainer> {
        override fun create(view: View) = MonthViewContainer(view)
        override fun bind(container: MonthViewContainer, data: CalendarMonth) {
            if (container.titlesContainer.tag == null) {
                container.titlesContainer.tag = data.yearMonth
                container.titlesContainer.children.map { it as TextView }
                    .forEachIndexed { index, textView ->
                        val dayOfWeek = daysOfWeek[index]
                        val title = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
                        textView.text = title
                    }
            }
        }
    }
  8. Handle Date Clicks and Selection

    main

    The library does not have a built-in 'selected' state. You must implement this by:

    1. Storing State: Keep a reference to the selectedDate (e.g., private var selectedDate: LocalDate? = null).
    2. Click Listener: Set a click listener on the view inside your DayViewContainer.
    3. Filtering Clicks: Only process clicks if day.position == DayPosition.MonthDate to avoid selecting InDate or OutDate cells.
    4. Updating UI: When a date is clicked, update your selectedDate variable and call calendarView.notifyDateChanged(date) for both the new selection and the previous selection to trigger a re-bind.
    5. Binding Logic: In dayBinder.bind(), check if data.date == selectedDate to apply background/text color changes.
    // Inside DayViewContainer
    init {
        view.setOnClickListener {
            if (day.position == DayPosition.MonthDate) {
                val currentSelection = selectedDate
                if (currentSelection == day.date) {
                    selectedDate = null
                    calendarView.notifyDateChanged(currentSelection)
                } else {
                    selectedDate = day.date
                    calendarView.notifyDateChanged(day.date)
                    if (currentSelection != null) {
                        calendarView.notifyDateChanged(currentSelection)
                    }
                }
            }
        }
    }
    
    // Inside dayBinder.bind()
    override fun bind(container: DayViewContainer, data: CalendarDay) {
        val day = data
        val textView = container.textView
        textView.text = day.date.dayOfMonth.toString()
        if (day.position == DayPosition.MonthDate) {
            textView.visibility = View.VISIBLE
            if (day.date == selectedDate) {
                textView.setTextColor(Color.WHITE)
                textView.setBackgroundResource(R.drawable.selection_background)
            } else {
                textView.setTextColor(Color.BLACK)
                textView.background = null
            }
        } else {
            textView.visibility = View.INVISIBLE
        }
    }
  9. Enable Java 8 API desugaring for minSdk below 26

    main

    If your Android app's minSdkVersion is below 26, you must enable Java 8+ API desugaring to support java.time classes. This requires Android Gradle plugin 4.0.0 or higher.

    1. Set coreLibraryDesugaringEnabled true in compileOptions.
    2. Set sourceCompatibility and targetCompatibility to JavaVersion.VERSION_1_8.
    3. Set jvmTarget to "1.8" in kotlinOptions.
    4. Add coreLibraryDesugaring dependency for com.android.tools:desugar_jdk_libs.
    android {
      defaultConfig {
        // Required ONLY if your minSdkVersion is below 21
        multiDexEnabled true
      }
    
      compileOptions {
        // Enable support for the new language APIs
        coreLibraryDesugaringEnabled true
        // Set Java compatibility (version can be higher if desired)
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
      }
    
      kotlinOptions {
        // Also add this for Kotlin projects (version can be higher if desired)
        jvmTarget = "1.8"
      }
    }
    
    dependencies {
      coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:<latest-version>'
    }
  10. Implement WeekCalendarView

    main

    To use the week-based WeekCalendarView, use LocalDate for setup instead of YearMonth.

    1. Add to XML: Use com.kizitonwose.calendar.view.WeekCalendarView.
    2. Setup: Call .setup() with a start date, end date, and the first day of the week.
    <com.kizitonwose.calendar.view.WeekCalendarView
        android:id="@+id/weekCalendarView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cv_dayViewResource="@layout/calendar_day_layout" />
    val currentDate = LocalDate.now()
    val currentMonth = YearMonth.now()
    val startDate = currentMonth.minusMonths(100).atStartOfMonth()
    val endDate = currentMonth.plusMonths(100).atEndOfMonth()
    val firstDayOfWeek = firstDayOfWeekFromLocale()
    weekCalendarView.setup(startDate, endDate, firstDayOfWeek)
    weekCalendarView.scrollToWeek(currentDate)
  11. Handle Day of Week Titles

    main

    To display weekday titles (e.g., Sun, Mon, Tue), use the daysOfWeek() helper function provided by the library. This ensures the titles match the calendar's firstDayOfWeek configuration.

    Static View Approach: Create a container (like a LinearLayout) above the calendar and populate its children with TextViews using the daysOfWeek list.

    // Get the list of days of week based on locale
    val daysOfWeek = daysOfWeek()
    
    // Use the first element to set up the calendar
    calendarView.setup(startMonth, endMonth, daysOfWeek.first())
    
    // Populate a static title container
    val titlesContainer = findViewById<ViewGroup>(R.id.titlesContainer)
    titlesContainer.children
        .map { it as TextView }
        .forEachIndexed { index, textView ->
            val dayOfWeek = daysOfWeek[index]
            val title = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
            textView.text = title
        }
  12. Implement Date Selection

    main

    The library does not manage selection state internally. To implement selection, maintain a mutableStateOf<LocalDate?> in your parent composable and pass the selection status into your Day composable. Use day.position == DayPosition.MonthDate to ensure only actual month dates are selectable.

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