diff --git a/docs/Compose.md b/docs/Compose.md
index e4955195..257c6d37 100644
--- a/docs/Compose.md
+++ b/docs/Compose.md
@@ -134,6 +134,35 @@ fun MainScreen() {
}
```
+`HorizontalYearCalendar` and `VerticalYearCalendar`:
+
+```kotlin
+@Composable
+fun MainScreen() {
+ val currentYear = remember { Year.now() }
+ val startYear = remember { currentYear.minusYears(100) } // Adjust as needed
+ val endYear = remember { currentYear.plusYears(100) } // Adjust as needed
+ val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library
+
+ val state = rememberYearCalendarState(
+ startYear = startYear,
+ endYear = endYear,
+ firstVisibleYear = currentYear,
+ firstDayOfWeek = firstDayOfWeek,
+ )
+ HorizontalYearCalendar(
+ state = state,
+ dayContent = { Day(it) },
+ )
+
+// If you need a vertical year calendar.
+// VerticalYearCalendar(
+// state = state,
+// dayContent = { Day(it) }
+// )
+}
+```
+
Your `Day` composable in its simplest form would be:
```kotlin
diff --git a/docs/View.md b/docs/View.md
index 8b167b28..995e530b 100644
--- a/docs/View.md
+++ b/docs/View.md
@@ -15,6 +15,7 @@
* [Date Selection](#date-selection)
* [Disabling dates](#disabling-dates)
- [Week view](#week-view)
+- [Year view](#year-view)
- [FAQ](#faq)
- [Migration](#migration)
@@ -32,16 +33,19 @@ Add the library to your project [here](https://github.com/kizitonwose/Calendar#s
## Class information
-The library can be used via two classes:
+The library can be used via three classes:
`CalendarView`: The typical month-based calendar.
`WeekCalendarView`: The week-based calendar.
-Both classes extend from `RecyclerView` so you can use all `RecyclerView` customizations like decorators etc.
+`YearCalendarView`: The year-based calendar.
-In the examples below, we will mostly use the `CalendarView` class since the two classes share the same basic concept. If you want a week-based calendar, replace `CalendarView` in your xml/code with `WeekCalendarView`.
-Most xml attributes and class properties/methods with the name prefix/suffix `month` (e.g `monthHeaderResource`) in the `CalendarView` will have an equivalent with the name prefix/suffix `week` (e.g `weekHeaderResource`) in the `WeekCalendarView`.
+These classes extend from `RecyclerView` so you can use all `RecyclerView` customizations like decorators etc.
+
+In the examples below, we will mostly use the `CalendarView` class since the three classes share the same basic concept. If you want a week-based calendar, replace `CalendarView` in your xml/code with `WeekCalendarView`. If you want a year-based calendar, replace `CalendarView` in your xml/code with `YearCalendarView`.
+
+Most xml attributes and class properties/methods with the name prefix/suffix `month` (e.g `monthHeaderResource`) in the `CalendarView` will have an equivalent with the name prefix/suffix `week` (e.g `weekHeaderResource`) in the `WeekCalendarView` and the name prefix/suffix `year` (e.g `yearHeaderResource`) in the `YearCalendarView`.
## Usage
@@ -103,8 +107,8 @@ Setup the desired dates in your Fragment or Activity:
**`CalendarView` setup:**
```kotlin
val currentMonth = YearMonth.now()
-val startMonth = currentMonth.minusMonths(100) // Adjust as needed
-val endMonth = currentMonth.plusMonths(100) // Adjust as needed
+val startMonth = currentMonth.minusMonths(100) // Adjust as needed
+val endMonth = currentMonth.plusMonths(100) // Adjust as needed
val firstDayOfWeek = firstDayOfWeekFromLocale() // Available from the library
calendarView.setup(startMonth, endMonth, firstDayOfWeek)
calendarView.scrollToMonth(currentMonth)
@@ -125,12 +129,32 @@ calendarView.scrollToMonth(currentMonth)
val currentDate = LocalDate.now()
val currentMonth = YearMonth.now()
val startDate = currentMonth.minusMonths(100).atStartOfMonth() // Adjust as needed
-val endDate = currentMonth.plusMonths(100).atEndOfMonth() // Adjust as needed
+val endDate = currentMonth.plusMonths(100).atEndOfMonth() // Adjust as needed
val firstDayOfWeek = firstDayOfWeekFromLocale() // Available from the library
weekCalendarView.setup(startDate, endDate, firstDayOfWeek)
weekCalendarView.scrollToWeek(currentDate)
```
+**`YearCalendarView` setup:**
+
+```diff
+-
+```
+
+```kotlin
+val currentYear = Year.now()
+val startYear = currentYear.minusYears(100) // Adjust as needed
+val endYear = currentYear.plusYears(100) // Adjust as needed
+val firstDayOfWeek = firstDayOfWeekFromLocale() // Available from the library
+yearCalendarView.setup(startYear, endYear, firstDayOfWeek)
+yearCalendarView.scrollToYear(currentYear)
+```
+
**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.
@@ -300,7 +324,7 @@ You can do more than just use the day titles as the header. For example, you can
#### XML (All prefixed `cv_` for clarity)
-**The following attributes are available for both `CalendarView` and `WeekCalendarView` classes:**
+**The following attributes are available for `CalendarView`, `WeekCalendarView` and `YearCalendarView` classes:**
- **dayViewResource**: The xml resource that is inflated and used as the day cell view. This must be provided.
@@ -313,15 +337,15 @@ You can do more than just use the day titles as the header. For example, you can
2. **seventhWidth**: Each day will have its width matching the width of the calendar divided by 7. The day is allowed to determine its height by setting a specific value or using `LayoutParams.WRAP_CONTENT`
3. **freeForm**: This day is allowed to determine its width and height by setting specific values or using `LayoutParams.WRAP_CONTENT`.
-**The following attributes are available for ONLY `CalendarView` class:**
+**The following attributes are available for `CalendarView` and `YearCalendarView` classes:**
-- **monthHeaderResource**: The xml resource that is inflated and used as a header for every month.
+- **monthHeaderResource**: The xml resource that is inflated and used as a header for each month.
-- **monthFooterResource**: The xml resource that is inflated and used as a footer for every month.
+- **monthFooterResource**: The xml resource that is inflated and used as a footer for each month.
- **orientation**: The calendar scroll direction, can be `horizontal` or `vertical`. Default is `horizontal`.
-- **monthViewClass**: A ViewGroup which is instantiated and used as the container for each month. This class must have a constructor which takes only a Context. You should exclude the name and constructor of this class from code obfuscation if enabled.
+- **monthViewClass**: A ViewGroup that is instantiated and used as the container for each month. This class must have a constructor which takes only a Context. You should exclude the name and constructor of this class from code obfuscation if enabled.
- **outDateStyle**: This determines how outDates are generated for each month on the calendar. 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.
@@ -353,6 +377,28 @@ calendarView.dayBinder = object : MonthDayBinder {
`monthDates` have their `position` property set to `DayPosition.MonthDate` as seen in the code snippet above.
+**The following attributes are available for ONLY `YearCalendarView` class:**
+
+- **yearHeaderResource**: The xml resource that is inflated and used as a header for each year.
+
+- **yearFooterResource**: The xml resource that is inflated and used as a footer for each year.
+
+- **yearViewClass**: A ViewGroup that is instantiated and used as the container for each year. This class must have a constructor which takes only a Context. You should exclude the name and constructor of this class from code obfuscation if enabled.
+
+- **monthColumns**: The number of month columns in each year. Must be from 1 to 12.
+
+- **monthHorizontalSpacing**: The horizontal spacing between month columns in each year.
+
+- **monthVerticalSpacing**: The vertical spacing between month rows in each year.
+
+- **monthHeight**: This determines how the height of each month row on the calendar is calculated. Can be one of two values:
+ 1. **followDaySize**: Each month row height is determined by the `daySize` value set on the calendar. Effectively, this is `wrap-content` if the value is `Square`,
+ `SeventhWidth`, or `FreeForm`, and will be equal to the calendar height divided by the number of rows if the value is `Rectangle`. When used together with `Rectangle`,
+ the calendar months and days will uniformly stretch to fill the parent's height.
+ 2. **fill**: Each month row height will be the calender height divided by the number of rows on the calendar. This means that the calendar months will be distributed
+ uniformly to fill the parent's height. However, the day content height will independently determine its height. This allows you to spread the calendar months evenly across the screen while
+ a `daySize` value of `Square` if you want square day content or `SeventhWidth` if you want to set a specific height value for the day content.
+
**The following attributes are available for ONLY `WeekCalendarView` class:**
- **weekHeaderResource**: The xml resource that is inflated and used as a header for every week.
@@ -371,9 +417,9 @@ All the respective XML attributes listed above are also available as properties
- **dayBinder**: An instance of `MonthDayBinder` for managing day cell views.
-- **monthHeaderBinder**: An instance of `MonthHeaderFooterBinder` for managing header views. The header view is shown above each month on the Calendar.
+- **monthHeaderBinder**: An instance of `MonthHeaderFooterBinder` for managing header views. The header view is shown above each month on the calendar.
-- **monthFooterBinder**: An instance of `MonthHeaderFooterBinder` for managing footer views. The footer view is shown below each month on the Calendar.
+- **monthFooterBinder**: An instance of `MonthHeaderFooterBinder` for managing footer views. The footer view is shown below each month on the calendar.
- **monthMargins**: The margins, in pixels to be applied on each month view. This can be used to add a space between two months.
@@ -383,12 +429,32 @@ All the respective XML attributes listed above are also available as properties
- **dayBinder**: An instance of `WeekDayBinder` for managing day cell views.
-- **weekHeaderBinder**: An instance of `WeekHeaderFooterBinder` for managing header views. The header view is shown above each week on the Calendar.
+- **weekHeaderBinder**: An instance of `WeekHeaderFooterBinder` for managing the header views shown above each week on the calendar.
-- **weekFooterBinder**: An instance of `WeekHeaderFooterBinder` for managing footer views. The footer view is shown below each week on the Calendar.
+- **weekFooterBinder**: An instance of `WeekHeaderFooterBinder` for managing the footer views shown below each week on the calendar.
- **weekMargins**: The margins, in pixels to be applied on each week view. This can be used to add a space between two weeks.
+**`YearCalendarView` properties:**
+
+- **yearScrollListener**: Called when the calendar scrolls to a new year. Mostly beneficial if `scrollPaged` is `true`.
+
+- **dayBinder**: An instance of `MonthDayBinder` for managing day cell views.
+
+- **monthHeaderBinder**: An instance of `MonthHeaderFooterBinder` for managing the header views shown above each month on the calendar.
+
+- **monthFooterBinder**: An instance of `MonthHeaderFooterBinder` for managing the footer views shown below each month on the calendar.
+
+- **monthMargins**: The margins, in pixels to be applied on each month view. This can be used to add a space between two months.
+
+- **yearHeaderBinder**: An instance of `YearHeaderFooterBinder` for managing the header views shown above each year on the calendar.
+
+- **yearFooterBinder**: An instance of `YearHeaderFooterBinder` for managing the footer views shown below each year on the calendar.
+
+- **yearMargins**: The margins, in pixels to be applied on each year view. This is the container in which the year header, body and footer are placed. For example, this can be used to add a space between two years.
+
+- **yearBodyMargins**: The margins, in pixels to be applied on each year body view. This is the grid in which the months in each year are shown, excluding the year header and footer.
+
### Methods
**`CalendarView` methods:**
@@ -403,11 +469,11 @@ All the respective XML attributes listed above are also available as properties
- **notifyCalendarChanged()**: Reload the entire calendar.
-- **findFirstVisibleMonth()** and **findLastVisibleMonth()**: Find the first and last visible months on the CalendarView respectively.
+- **findFirstVisibleMonth()** and **findLastVisibleMonth()**: Find the first and last visible months on the calendar respectively.
-- **findFirstVisibleDay()** and **findLastVisibleDay()**: Find the first and last visible days on the CalendarView respectively.
+- **findFirstVisibleDay()** and **findLastVisibleDay()**: Find the first and last visible days on the calendar respectively.
-- **updateMonthData()**: Update the CalendarView's start month or end month or the first day of week after the initial setup. The currently visible month is preserved. The calendar can handle really large date ranges so you may want to setup the calendar with a large date range instead of updating the range frequently.
+- **updateMonthData()**: Update the calendar's start month or end month or the first day of week after the initial setup. The currently visible month is preserved. The calendar can handle really large date ranges so you may want to setup the calendar with a large date range instead of updating the range frequently.
**`WeekCalendarView` methods:**
@@ -425,11 +491,34 @@ All the respective XML attributes listed above are also available as properties
- **findFirstVisibleDay()** and **findLastVisibleDay()**: Find the first and last visible days on the calendar respectively.
-- **updateWeekData()**: Update the WeeCalendarView's start date or end date or the first day of week after the initial setup. The currently visible week is preserved. The calendar can handle really large date ranges so you may want to setup the calendar with a large date range instead of updating the range frequently.
+- **updateWeekData()**: Update the calendar's start date or end date or the first day of week after the initial setup. The currently visible week is preserved. The calendar can handle really large date ranges so you may want to setup the calendar with a large date range instead of updating the range frequently.
+
+**`YearCalendarView` methods:**
+
+- **scrollToDate(date: LocalDate)**: Scroll to a specific date on the calendar. Use `smoothScrollToDate()` to get a smooth scrolling animation.
+
+- **scrollToMonth(month: YearMonth)**: Scroll to a month on the calendar. Use `smoothScrollToMonth()` to get a smooth scrolling animation.
+
+- **scrollToYear(year: Year)**: Scroll to a year on the calendar. Use `smoothScrollToYear()` to get a smooth scrolling animation.
+
+- **notifyDateChanged(date: LocalDate)**: Reload the view for the specified date.
+
+- **notifyMonthChanged(month: YearMonth)**: Reload the header, body and footer views for the specified month.
+
+- **notifyYearChanged(year: Year)**: Reload the header, body (all months in the year) and footer views for the specified year.
+
+- **notifyCalendarChanged()**: Reload the entire calendar.
+
+- **findFirstVisibleYear()** and **findLastVisibleYear()**: Find the first and last visible years on the calendar respectively.
+
+- **findFirstVisibleMonth()** and **findLastVisibleMonth()**: Find the first and last visible months on the calendar respectively.
+
+- **findFirstVisibleDay()** and **findLastVisibleDay()**: Find the first and last visible days on the calendar respectively.
+
+- **updateYearData()**: Update the calendar's start year or end year or the first day of week after the initial setup. The currently visible year is preserved. The calendar can handle really large date ranges so you may want to setup the calendar with a large date range instead of updating the range frequently.
There's no need to list all available methods or repeat the documentation here. Please see
-the [CalendarView](https://github.com/kizitonwose/Calendar/blob/main/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt)
-and [WeekCalendarView](https://github.com/kizitonwose/Calendar/blob/main/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt)
+the [CalendarView](https://github.com/kizitonwose/Calendar/blob/main/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt), [WeekCalendarView](https://github.com/kizitonwose/Calendar/blob/main/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt) and [YearCalendarView](https://github.com/kizitonwose/Calendar/blob/main/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt)
classes for all properties and methods available with proper documentation.
### Date clicks
@@ -490,7 +579,7 @@ calendarView.dayBinder = object : MonthDayBinder {
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 date binder. Remember that since CalendarView and WeekCalendarView extend from RecyclerView, you need to undo any special effects on dates where it is not needed.
+Implementing date selection is as simple as showing a background on a specific date in the date binder. Remember that since CalendarView, WeekCalendarView and YearCalendarView all extend from RecyclerView, you need to undo any special effects on dates where it is not needed.
For this example, I want only the last clicked date to be selected on the calendar.
@@ -598,7 +687,7 @@ See the sample project for some complex implementations.
## Week view
-As discussed previously, the library provides two classes `CalendarView` and `WeekCalendarView`. The `WeekCalendarView` class is a week-based calendar implementation. Almost all topics covered above for the month calendar will apply to the week calendar. The main difference is that the xml attributes and class properties/methods will have a slightly different name, typically with a `week` prefix/suffix instead of `month`.
+The `WeekCalendarView` class is a week-based calendar implementation. Almost all topics covered above for the month calendar will apply to the week calendar. The main difference is that the xml attributes and class properties/methods will have a slightly different name, typically with a `week` prefix/suffix instead of `month`.
For example: `monthHeaderResource` => `weekHeaderResource`, `scrollToMonth()` => `scrollToWeek()`, `findFirstVisibleMonth()` => `findFirstVisibleWeek()` and many others, but you get the idea.
@@ -624,6 +713,28 @@ A week calendar implementation from the sample app:
+## Year view
+
+The `YearCalendarView` class is a year-based calendar implementation. All topics covered above for the month calendar will apply to the year calendar. The year calendar also has additional xml attributes and class properties/methods, typically with a `year` prefix/suffix.
+
+For example: `yearHeaderResource`, `scrollToYear()`, `findFirstVisibleYear()` and many others, but you get the idea.
+
+To show the year calendar in your layout, add the view:
+
+```xml
+
+```
+
+Then follow the setup instructions above to provide a day resource/binder etc as you would do for the month calendar.
+
+A year calendar implementation from 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](https://github.com/kizitonwose/Calendar/issues/1).**
@@ -632,7 +743,7 @@ Remember that all the screenshots shown so far are just examples of what you can
**Q**: How do I use this library in a Java project?
-**A**: It works out of the box, however, the `MonthScrollListener` is not an interface but a Kotlin function. To set the `MonthScrollListener` in a Java project see [this](https://github.com/kizitonwose/Calendar/issues/74).
+**A**: It works out of the box, however, the `MonthScrollListener`, `WeekScrollListener` and `YearScrollListener` are not interfaces but Kotlin functions. To set the listener in a Java project see [this](https://github.com/kizitonwose/Calendar/issues/74).
**Q**: How do I disable user scrolling on the calendar so I can only scroll programmatically?
diff --git a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt
index 1fc86863..27d7ca76 100644
--- a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt
+++ b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt
@@ -236,11 +236,12 @@ class CalendarViewTest {
@Test
fun findVisibleDaysAndMonthsWorksOnVerticalOrientation() {
- openExampleAt(1)
+ openExampleAt(7)
- val calendarView = getView(R.id.exTwoCalendar)
+ val calendarView = getView(R.id.exEightCalendar)
runOnMain {
+ calendarView.orientation = RecyclerView.VERTICAL
// Scroll to a random date
calendarView.scrollToDate(LocalDate.now().plusDays(120))
}
@@ -266,11 +267,12 @@ class CalendarViewTest {
@Test
fun findVisibleDaysAndMonthsWorksOnHorizontalOrientation() {
- openExampleAt(0)
+ openExampleAt(7)
- val calendarView = getView(R.id.exOneCalendar)
+ val calendarView = getView(R.id.exEightCalendar)
runOnMain {
+ calendarView.orientation = RecyclerView.HORIZONTAL
// Scroll to a random date
calendarView.scrollToDate(LocalDate.now().plusDays(120))
}
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/BaseFragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/BaseFragment.kt
index 974a5afc..e55fbc53 100644
--- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/BaseFragment.kt
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/BaseFragment.kt
@@ -1,9 +1,13 @@
package com.kizitonwose.calendar.sample.view
+import android.os.Bundle
+import android.view.View
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
+import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
import com.kizitonwose.calendar.sample.R
interface HasToolbar {
@@ -18,6 +22,13 @@ abstract class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes) {
val activityToolbar: Toolbar
get() = (requireActivity() as CalendarViewActivity).binding.activityToolbar
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ if (this is MenuProvider) {
+ requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.CREATED)
+ }
+ }
+
override fun onStart() {
super.onStart()
if (this is HasToolbar) {
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/CalendarViewOptionsAdapter.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/CalendarViewOptionsAdapter.kt
index e72c1f70..916dcdd2 100644
--- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/CalendarViewOptionsAdapter.kt
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/CalendarViewOptionsAdapter.kt
@@ -78,6 +78,16 @@ class CalendarViewOptionsAdapter(val onClick: (ExampleItem) -> Unit) :
R.string.example_8_subtitle,
horizontal,
) { Example8Fragment() },
+ ExampleItem(
+ R.string.example_9_title,
+ R.string.example_9_subtitle,
+ horizontal,
+ ) { Example9Fragment() },
+ ExampleItem(
+ R.string.example_10_title,
+ R.string.example_10_subtitle,
+ horizontal,
+ ) { Example10Fragment() },
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionsViewHolder {
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt
new file mode 100644
index 00000000..4da0857f
--- /dev/null
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt
@@ -0,0 +1,180 @@
+package com.kizitonwose.calendar.sample.view
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.MenuProvider
+import androidx.core.view.children
+import androidx.core.view.updatePaddingRelative
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.core.DayPosition
+import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
+import com.kizitonwose.calendar.sample.R
+import com.kizitonwose.calendar.sample.databinding.Example10FragmentBinding
+import com.kizitonwose.calendar.sample.databinding.Example9CalendarDayBinding
+import com.kizitonwose.calendar.sample.databinding.Example9CalendarMonthHeaderBinding
+import com.kizitonwose.calendar.sample.shared.displayText
+import com.kizitonwose.calendar.view.MarginValues
+import com.kizitonwose.calendar.view.MonthDayBinder
+import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
+import com.kizitonwose.calendar.view.ViewContainer
+import java.time.LocalDate
+import java.time.Year
+
+class Example10Fragment : BaseFragment(R.layout.example_10_fragment), HasToolbar, HasBackButton, MenuProvider {
+ override val toolbar: Toolbar
+ get() = binding.exTenToolbar
+
+ override val titleRes: Int = R.string.example_10_title
+
+ private lateinit var binding: Example10FragmentBinding
+
+ private var selectedDate: LocalDate? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding = Example10FragmentBinding.bind(view)
+ val config = requireContext().resources.configuration
+ val isTablet = config.smallestScreenWidthDp >= 600
+
+ configureBinders(isTablet)
+
+ binding.exTenToolbar.updatePaddingRelative(end = dpToPx(if (isTablet) 42 else 6, requireContext()))
+
+ binding.exTenCalendar.apply {
+ val currentYear = Year.now()
+ monthVerticalSpacing = dpToPx(20, requireContext())
+ monthHorizontalSpacing = dpToPx(if (isTablet) 52 else 10, requireContext())
+ yearMargins = MarginValues(
+ vertical = dpToPx(if (isTablet) 20 else 6, requireContext()),
+ horizontal = dpToPx(if (isTablet) 52 else 14, requireContext()),
+ )
+ yearScrollListener = { year ->
+ binding.exTenToolbar.title = year.year.value.toString()
+ }
+ setup(
+ currentYear.minusYears(100),
+ currentYear.plusYears(100),
+ firstDayOfWeekFromLocale(),
+ )
+ scrollToYear(currentYear)
+ }
+ }
+
+ private fun configureBinders(isTablet: Boolean) {
+ val calendarView = binding.exTenCalendar
+
+ class DayViewContainer(view: View) : ViewContainer(view) {
+ // Will be set when this container is bound. See the dayBinder.
+ lateinit var day: CalendarDay
+ val textView = Example9CalendarDayBinding.bind(view).exNineDayText.apply {
+ textSize = if (isTablet) 10f else 9f
+ }
+
+ init {
+ textView.setOnClickListener {
+ if (day.position == DayPosition.MonthDate) {
+ if (selectedDate == day.date) {
+ selectedDate = null
+ calendarView.notifyDayChanged(day)
+ } else {
+ val oldDate = selectedDate
+ selectedDate = day.date
+ calendarView.notifyDateChanged(day.date)
+ oldDate?.let { calendarView.notifyDateChanged(oldDate) }
+ }
+ }
+ }
+ }
+ }
+
+ calendarView.dayBinder = object : MonthDayBinder {
+ override fun create(view: View) = DayViewContainer(view)
+ override fun bind(container: DayViewContainer, data: CalendarDay) {
+ container.day = data
+ val textView = container.textView
+ textView.text = data.date.dayOfMonth.toString()
+
+ if (data.position == DayPosition.MonthDate) {
+ textView.makeVisible()
+ when (data.date) {
+ selectedDate -> {
+ textView.setTextColorRes(R.color.example_2_white)
+ textView.setBackgroundResource(R.drawable.example_2_selected_bg)
+ }
+
+ else -> {
+ textView.setTextColorRes(R.color.example_2_black)
+ textView.background = null
+ }
+ }
+ } else {
+ textView.makeInVisible()
+ }
+ }
+ }
+
+ val monthNameTypeFace = Typeface.semiBold(requireContext())
+
+ class MonthViewContainer(view: View) : ViewContainer(view) {
+ val bind = Example9CalendarMonthHeaderBinding.bind(view)
+ val textView = bind.exNineMonthHeaderText.apply {
+ setTypeface(monthNameTypeFace)
+ textSize = if (isTablet) 16f else 14f
+ updatePaddingRelative(start = dpToPx(if (isTablet) 10 else 6, requireContext()))
+ }
+ val legendLayout = bind.legendLayout.root
+ }
+
+ val legendTypeface = Typeface.medium(requireContext())
+
+ calendarView.monthHeaderBinder =
+ object : MonthHeaderFooterBinder {
+ override fun create(view: View) = MonthViewContainer(view)
+ override fun bind(container: MonthViewContainer, data: CalendarMonth) {
+ container.textView.text = data.yearMonth.month.displayText(short = false)
+ // Setup each header day text if we have not done that already.
+ if (container.legendLayout.tag == null) {
+ container.legendLayout.tag = true
+ val daysOfWeek = data.weekDays.first().map { it.date.dayOfWeek }
+ container.legendLayout.children.map { it as TextView }
+ .forEachIndexed { index, tv ->
+ tv.text = daysOfWeek[index].displayText(uppercase = true, narrow = true)
+ tv.setTextColorRes(R.color.example_3_black)
+ tv.textSize = if (isTablet) 14f else 11f
+ tv.setTypeface(legendTypeface)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.example_10_menu, menu)
+ }
+
+ override fun onMenuItemSelected(item: MenuItem): Boolean = with(binding.exTenCalendar) {
+ return when (item.itemId) {
+ R.id.menuItemPrevious -> {
+ findFirstVisibleYear()?.year?.let { visibleYear ->
+ smoothScrollToYear(visibleYear.minusYears(1))
+ }
+ true
+ }
+
+ R.id.menuItemNext -> {
+ findFirstVisibleYear()?.year?.let { visibleYear ->
+ smoothScrollToYear(visibleYear.plusYears(1))
+ }
+ true
+ }
+
+ else -> false
+ }
+ }
+}
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example2Fragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example2Fragment.kt
index f242580c..127316b5 100644
--- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example2Fragment.kt
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example2Fragment.kt
@@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
+import androidx.core.view.MenuProvider
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import com.kizitonwose.calendar.core.CalendarDay
@@ -25,7 +26,7 @@ import java.time.LocalDate
import java.time.YearMonth
import java.time.format.DateTimeFormatter
-class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar, HasBackButton {
+class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar, HasBackButton, MenuProvider {
override val toolbar: Toolbar
get() = binding.exTwoToolbar
@@ -38,7 +39,6 @@ class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar,
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- setHasOptionsMenu(true)
binding = Example2FragmentBinding.bind(view)
val daysOfWeek = daysOfWeek()
binding.legendLayout.root.children.forEachIndexed { index, child ->
@@ -57,22 +57,20 @@ class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar,
}
private lateinit var menuItem: MenuItem
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.example_2_menu, menu)
- menuItem = menu.getItem(0)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == R.id.menuItemDone) {
- val date = selectedDate ?: return false
+ menuItem = menu.findItem(R.id.menuItemDone)
+ menuItem.setOnMenuItemClickListener click@{
+ val date = selectedDate ?: return@click true
val text = "Selected: ${DateTimeFormatter.ofPattern("d MMMM yyyy").format(date)}"
Snackbar.make(requireView(), text, Snackbar.LENGTH_SHORT).show()
parentFragmentManager.popBackStack()
- return true
+ return@click true
}
- return super.onOptionsItemSelected(item)
}
+ override fun onMenuItemSelected(item: MenuItem): Boolean = true
+
private fun configureBinders() {
val calendarView = binding.exTwoCalendar
@@ -113,10 +111,12 @@ class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar,
textView.setTextColorRes(R.color.example_2_white)
textView.setBackgroundResource(R.drawable.example_2_selected_bg)
}
+
today -> {
textView.setTextColorRes(R.color.example_2_red)
textView.background = null
}
+
else -> {
textView.setTextColorRes(R.color.example_2_black)
textView.background = null
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt
index b105d78b..948cce39 100644
--- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt
@@ -6,12 +6,14 @@ import android.os.Bundle
import android.util.TypedValue
import android.view.Menu
import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
+import androidx.core.view.MenuProvider
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import com.kizitonwose.calendar.core.CalendarDay
@@ -35,7 +37,7 @@ import java.time.LocalDate
import java.time.YearMonth
import java.time.format.DateTimeFormatter
-class Example4Fragment : BaseFragment(R.layout.example_4_fragment), HasToolbar, HasBackButton {
+class Example4Fragment : BaseFragment(R.layout.example_4_fragment), HasToolbar, HasBackButton, MenuProvider {
override val toolbar: Toolbar
get() = binding.exFourToolbar
@@ -52,7 +54,6 @@ class Example4Fragment : BaseFragment(R.layout.example_4_fragment), HasToolbar,
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addStatusBarColorUpdate(R.color.white)
- setHasOptionsMenu(true)
binding = Example4FragmentBinding.bind(view)
// Set the First day of week depending on Locale
val daysOfWeek = daysOfWeek()
@@ -109,7 +110,7 @@ class Example4Fragment : BaseFragment(R.layout.example_4_fragment), HasToolbar,
binding.exFourSaveButton.isEnabled = selection.daysBetween != null
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.example_4_menu, menu)
binding.exFourToolbar.post {
// Configure menu text to match what is in the Airbnb app.
@@ -127,6 +128,8 @@ class Example4Fragment : BaseFragment(R.layout.example_4_fragment), HasToolbar,
}
}
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean = true
+
override fun onStart() {
super.onStart()
val closeIndicator = requireContext().getDrawableCompat(R.drawable.ic_close).apply {
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt
new file mode 100644
index 00000000..4563366f
--- /dev/null
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt
@@ -0,0 +1,167 @@
+package com.kizitonwose.calendar.sample.view
+
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.children
+import androidx.core.view.updatePadding
+import androidx.core.view.updatePaddingRelative
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.core.CalendarYear
+import com.kizitonwose.calendar.core.DayPosition
+import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
+import com.kizitonwose.calendar.sample.R
+import com.kizitonwose.calendar.sample.databinding.Example9CalendarDayBinding
+import com.kizitonwose.calendar.sample.databinding.Example9CalendarMonthHeaderBinding
+import com.kizitonwose.calendar.sample.databinding.Example9CalendarYearHeaderBinding
+import com.kizitonwose.calendar.sample.databinding.Example9FragmentBinding
+import com.kizitonwose.calendar.sample.shared.displayText
+import com.kizitonwose.calendar.view.MarginValues
+import com.kizitonwose.calendar.view.MonthDayBinder
+import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
+import com.kizitonwose.calendar.view.ViewContainer
+import com.kizitonwose.calendar.view.YearHeaderFooterBinder
+import java.time.LocalDate
+import java.time.Year
+import java.time.YearMonth
+
+class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, HasBackButton {
+ override val toolbar: Toolbar
+ get() = binding.exNineToolbar
+
+ override val titleRes: Int = R.string.example_9_title
+
+ private lateinit var binding: Example9FragmentBinding
+
+ private var selectedDate: LocalDate? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding = Example9FragmentBinding.bind(view)
+ val config = requireContext().resources.configuration
+ val isTablet = config.smallestScreenWidthDp >= 600
+
+ configureBinders(isTablet)
+
+ binding.exNineCalendar.apply {
+ val currentMonth = YearMonth.now()
+ monthVerticalSpacing = dpToPx(20, requireContext())
+ monthHorizontalSpacing = dpToPx(if (isTablet) 52 else 10, requireContext())
+ yearMargins = MarginValues(
+ horizontal = dpToPx(if (isTablet) 52 else 14, requireContext()),
+ )
+ isMonthVisible = {
+ it.yearMonth >= currentMonth
+ }
+ setup(
+ Year.of(currentMonth.year),
+ Year.of(currentMonth.year).plusYears(50),
+ firstDayOfWeekFromLocale(),
+ )
+ }
+ }
+
+ private fun configureBinders(isTablet: Boolean) {
+ val calendarView = binding.exNineCalendar
+
+ class DayViewContainer(view: View) : ViewContainer(view) {
+ // Will be set when this container is bound. See the dayBinder.
+ lateinit var day: CalendarDay
+ val textView = Example9CalendarDayBinding.bind(view).exNineDayText.apply {
+ textSize = if (isTablet) 10f else 9f
+ }
+
+ init {
+ textView.setOnClickListener {
+ if (day.position == DayPosition.MonthDate) {
+ if (selectedDate == day.date) {
+ selectedDate = null
+ calendarView.notifyDayChanged(day)
+ } else {
+ val oldDate = selectedDate
+ selectedDate = day.date
+ calendarView.notifyDateChanged(day.date)
+ oldDate?.let { calendarView.notifyDateChanged(oldDate) }
+ }
+ }
+ }
+ }
+ }
+
+ calendarView.dayBinder = object : MonthDayBinder {
+ override fun create(view: View) = DayViewContainer(view)
+ override fun bind(container: DayViewContainer, data: CalendarDay) {
+ container.day = data
+ val textView = container.textView
+ textView.text = data.date.dayOfMonth.toString()
+
+ if (data.position == DayPosition.MonthDate) {
+ textView.makeVisible()
+ when (data.date) {
+ selectedDate -> {
+ textView.setTextColorRes(R.color.example_2_white)
+ textView.setBackgroundResource(R.drawable.example_2_selected_bg)
+ }
+
+ else -> {
+ textView.setTextColorRes(R.color.example_2_black)
+ textView.background = null
+ }
+ }
+ } else {
+ textView.makeInVisible()
+ }
+ }
+ }
+
+ val monthNameTypeFace = Typeface.semiBold(requireContext())
+
+ class MonthViewContainer(view: View) : ViewContainer(view) {
+ val bind = Example9CalendarMonthHeaderBinding.bind(view)
+ val textView = bind.exNineMonthHeaderText.apply {
+ setTypeface(monthNameTypeFace)
+ textSize = if (isTablet) 16f else 14f
+ updatePaddingRelative(start = dpToPx(if (isTablet) 10 else 6, requireContext()))
+ }
+ val legendLayout = bind.legendLayout.root
+ }
+
+ val legendTypeface = Typeface.medium(requireContext())
+
+ calendarView.monthHeaderBinder =
+ object : MonthHeaderFooterBinder {
+ override fun create(view: View) = MonthViewContainer(view)
+ override fun bind(container: MonthViewContainer, data: CalendarMonth) {
+ container.textView.text = data.yearMonth.month.displayText(short = false)
+ // Setup each header day text if we have not done that already.
+ if (container.legendLayout.tag == null) {
+ container.legendLayout.tag = true
+ val daysOfWeek = data.weekDays.first().map { it.date.dayOfWeek }
+ container.legendLayout.children.map { it as TextView }
+ .forEachIndexed { index, tv ->
+ tv.text = daysOfWeek[index].displayText(uppercase = true, narrow = true)
+ tv.setTextColorRes(R.color.example_3_black)
+ tv.textSize = if (isTablet) 14f else 11f
+ tv.setTypeface(legendTypeface)
+ }
+ }
+ }
+ }
+
+ class YearViewContainer(view: View) : ViewContainer(view) {
+ val textView = Example9CalendarYearHeaderBinding.bind(view).exNineYearHeaderText.apply {
+ textSize = if (isTablet) 52f else 44f
+ updatePadding(bottom = dpToPx(if (isTablet) 16 else 10, requireContext()))
+ }
+ }
+ calendarView.yearHeaderBinder =
+ object : YearHeaderFooterBinder {
+ override fun create(view: View) = YearViewContainer(view)
+ override fun bind(container: YearViewContainer, data: CalendarYear) {
+ container.textView.text = data.year.value.toString()
+ }
+ }
+ }
+}
diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Typeface.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Typeface.kt
new file mode 100644
index 00000000..dd5cc80c
--- /dev/null
+++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Typeface.kt
@@ -0,0 +1,16 @@
+package com.kizitonwose.calendar.sample.view
+
+import android.content.Context
+import android.graphics.Typeface
+import androidx.core.graphics.TypefaceCompat
+
+/**
+ * See the comment on the [TypefaceCompat.create] method
+ * with the weight param for various weight definitions.
+ */
+object Typeface {
+ fun normal(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 400, false)
+ fun medium(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 500, false)
+ fun semiBold(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 600, false)
+ fun bold(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 700, false)
+}
diff --git a/sample/src/main/res/layout/example_10_fragment.xml b/sample/src/main/res/layout/example_10_fragment.xml
new file mode 100644
index 00000000..9eee5b2a
--- /dev/null
+++ b/sample/src/main/res/layout/example_10_fragment.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/layout/example_9_calendar_day.xml b/sample/src/main/res/layout/example_9_calendar_day.xml
new file mode 100644
index 00000000..3821e745
--- /dev/null
+++ b/sample/src/main/res/layout/example_9_calendar_day.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/sample/src/main/res/layout/example_9_calendar_month_header.xml b/sample/src/main/res/layout/example_9_calendar_month_header.xml
new file mode 100644
index 00000000..ebc195d8
--- /dev/null
+++ b/sample/src/main/res/layout/example_9_calendar_month_header.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/layout/example_9_calendar_year_header.xml b/sample/src/main/res/layout/example_9_calendar_year_header.xml
new file mode 100644
index 00000000..25d6caab
--- /dev/null
+++ b/sample/src/main/res/layout/example_9_calendar_year_header.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/layout/example_9_fragment.xml b/sample/src/main/res/layout/example_9_fragment.xml
new file mode 100644
index 00000000..6ade8fec
--- /dev/null
+++ b/sample/src/main/res/layout/example_9_fragment.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/menu/example_10_menu.xml b/sample/src/main/res/menu/example_10_menu.xml
new file mode 100644
index 00000000..fcee5a4d
--- /dev/null
+++ b/sample/src/main/res/menu/example_10_menu.xml
@@ -0,0 +1,19 @@
+
+
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
index 68fe6b87..c9696921 100644
--- a/sample/src/main/res/values/strings.xml
+++ b/sample/src/main/res/values/strings.xml
@@ -17,6 +17,10 @@
Week calendar with single selection, paged scroll and visible item observation.
Example 8
Fullscreen calendar, header and footer views, paged horizontal scrolling, shows the \"Rectangle\" DaySize option.
+ Example 9
+ Vertical year calendar - Hidden past months with continuous scroll. Best suited for large screens.
+ Example 10
+ Horizontal year calendar - Paged scrolling, shows the \"Fill\" implementation of MonthHeight property. Best suited for large screens.
Save
Close
Enter event title
diff --git a/view/api/view.api b/view/api/view.api
index fdc70dcb..c7b0741c 100644
--- a/view/api/view.api
+++ b/view/api/view.api
@@ -73,9 +73,12 @@ public final class com/kizitonwose/calendar/view/DaySize : java/lang/Enum {
}
public final class com/kizitonwose/calendar/view/MarginValues {
+ public static final field Companion Lcom/kizitonwose/calendar/view/MarginValues$Companion;
public fun ()V
+ public fun (II)V
public fun (IIII)V
public synthetic fun (IIIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()I
public final fun component2 ()I
public final fun component3 ()I
@@ -91,12 +94,24 @@ public final class com/kizitonwose/calendar/view/MarginValues {
public fun toString ()Ljava/lang/String;
}
+public final class com/kizitonwose/calendar/view/MarginValues$Companion {
+ public final fun getZERO ()Lcom/kizitonwose/calendar/view/MarginValues;
+}
+
public abstract interface class com/kizitonwose/calendar/view/MonthDayBinder : com/kizitonwose/calendar/view/Binder {
}
public abstract interface class com/kizitonwose/calendar/view/MonthHeaderFooterBinder : com/kizitonwose/calendar/view/Binder {
}
+public final class com/kizitonwose/calendar/view/MonthHeight : java/lang/Enum {
+ public static final field Fill Lcom/kizitonwose/calendar/view/MonthHeight;
+ public static final field FollowDaySize Lcom/kizitonwose/calendar/view/MonthHeight;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lcom/kizitonwose/calendar/view/MonthHeight;
+ public static fun values ()[Lcom/kizitonwose/calendar/view/MonthHeight;
+}
+
public class com/kizitonwose/calendar/view/ViewContainer {
public fun (Landroid/view/View;)V
public final fun getView ()Landroid/view/View;
@@ -159,3 +174,92 @@ public abstract interface class com/kizitonwose/calendar/view/WeekDayBinder : co
public abstract interface class com/kizitonwose/calendar/view/WeekHeaderFooterBinder : com/kizitonwose/calendar/view/Binder {
}
+public class com/kizitonwose/calendar/view/YearCalendarView : androidx/recyclerview/widget/RecyclerView {
+ public fun (Landroid/content/Context;)V
+ public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V
+ public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V
+ public final fun findFirstVisibleDay ()Lcom/kizitonwose/calendar/core/CalendarDay;
+ public final fun findFirstVisibleMonth ()Lcom/kizitonwose/calendar/core/CalendarMonth;
+ public final fun findFirstVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear;
+ public final fun findLastVisibleDay ()Lcom/kizitonwose/calendar/core/CalendarDay;
+ public final fun findLastVisibleMonth ()Lcom/kizitonwose/calendar/core/CalendarMonth;
+ public final fun findLastVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear;
+ public final fun getDayBinder ()Lcom/kizitonwose/calendar/view/MonthDayBinder;
+ public final fun getDaySize ()Lcom/kizitonwose/calendar/view/DaySize;
+ public final fun getDayViewResource ()I
+ public final fun getMonthColumns ()I
+ public final fun getMonthFooterBinder ()Lcom/kizitonwose/calendar/view/MonthHeaderFooterBinder;
+ public final fun getMonthFooterResource ()I
+ public final fun getMonthHeaderBinder ()Lcom/kizitonwose/calendar/view/MonthHeaderFooterBinder;
+ public final fun getMonthHeaderResource ()I
+ public final fun getMonthHeight ()Lcom/kizitonwose/calendar/view/MonthHeight;
+ public final fun getMonthHorizontalSpacing ()I
+ public final fun getMonthVerticalSpacing ()I
+ public final fun getMonthViewClass ()Ljava/lang/String;
+ public final fun getOrientation ()I
+ public final fun getOutDateStyle ()Lcom/kizitonwose/calendar/core/OutDateStyle;
+ public final fun getScrollPaged ()Z
+ public final fun getYearBodyMargins ()Lcom/kizitonwose/calendar/view/MarginValues;
+ public final fun getYearFooterBinder ()Lcom/kizitonwose/calendar/view/YearHeaderFooterBinder;
+ public final fun getYearFooterResource ()I
+ public final fun getYearHeaderBinder ()Lcom/kizitonwose/calendar/view/YearHeaderFooterBinder;
+ public final fun getYearHeaderResource ()I
+ public final fun getYearMargins ()Lcom/kizitonwose/calendar/view/MarginValues;
+ public final fun getYearScrollListener ()Lkotlin/jvm/functions/Function1;
+ public final fun getYearViewClass ()Ljava/lang/String;
+ public final fun isMonthVisible ()Lkotlin/jvm/functions/Function1;
+ public final fun notifyCalendarChanged ()V
+ public final fun notifyDateChanged (Ljava/time/LocalDate;)V
+ public final fun notifyDateChanged (Ljava/time/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;)V
+ public final fun notifyDateChanged (Ljava/time/LocalDate;[Lcom/kizitonwose/calendar/core/DayPosition;)V
+ public static synthetic fun notifyDateChanged$default (Lcom/kizitonwose/calendar/view/YearCalendarView;Ljava/time/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;ILjava/lang/Object;)V
+ public final fun notifyDayChanged (Lcom/kizitonwose/calendar/core/CalendarDay;)V
+ public final fun notifyMonthChanged (Ljava/time/YearMonth;)V
+ public final fun notifyYearChanged (Ljava/time/Year;)V
+ public final fun scrollToDate (Ljava/time/LocalDate;)V
+ public final fun scrollToDate (Ljava/time/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;)V
+ public static synthetic fun scrollToDate$default (Lcom/kizitonwose/calendar/view/YearCalendarView;Ljava/time/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;ILjava/lang/Object;)V
+ public final fun scrollToDay (Lcom/kizitonwose/calendar/core/CalendarDay;)V
+ public final fun scrollToMonth (Ljava/time/YearMonth;)V
+ public final fun scrollToYear (Ljava/time/Year;)V
+ public final fun setDayBinder (Lcom/kizitonwose/calendar/view/MonthDayBinder;)V
+ public final fun setDaySize (Lcom/kizitonwose/calendar/view/DaySize;)V
+ public final fun setDayViewResource (I)V
+ public final fun setMonthColumns (I)V
+ public final fun setMonthFooterBinder (Lcom/kizitonwose/calendar/view/MonthHeaderFooterBinder;)V
+ public final fun setMonthFooterResource (I)V
+ public final fun setMonthHeaderBinder (Lcom/kizitonwose/calendar/view/MonthHeaderFooterBinder;)V
+ public final fun setMonthHeaderResource (I)V
+ public final fun setMonthHeight (Lcom/kizitonwose/calendar/view/MonthHeight;)V
+ public final fun setMonthHorizontalSpacing (I)V
+ public final fun setMonthVerticalSpacing (I)V
+ public final fun setMonthViewClass (Ljava/lang/String;)V
+ public final fun setMonthVisible (Lkotlin/jvm/functions/Function1;)V
+ public final fun setOrientation (I)V
+ public final fun setOutDateStyle (Lcom/kizitonwose/calendar/core/OutDateStyle;)V
+ public final fun setScrollPaged (Z)V
+ public final fun setYearBodyMargins (Lcom/kizitonwose/calendar/view/MarginValues;)V
+ public final fun setYearFooterBinder (Lcom/kizitonwose/calendar/view/YearHeaderFooterBinder;)V
+ public final fun setYearFooterResource (I)V
+ public final fun setYearHeaderBinder (Lcom/kizitonwose/calendar/view/YearHeaderFooterBinder;)V
+ public final fun setYearHeaderResource (I)V
+ public final fun setYearMargins (Lcom/kizitonwose/calendar/view/MarginValues;)V
+ public final fun setYearScrollListener (Lkotlin/jvm/functions/Function1;)V
+ public final fun setYearViewClass (Ljava/lang/String;)V
+ public final fun setup (Ljava/time/Year;Ljava/time/Year;Ljava/time/DayOfWeek;)V
+ public final fun smoothScrollToDate (Ljava/time/LocalDate;)V
+ public final fun smoothScrollToDate (Ljava/time/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;)V
+ public static synthetic fun smoothScrollToDate$default (Lcom/kizitonwose/calendar/view/YearCalendarView;Ljava/time/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;ILjava/lang/Object;)V
+ public final fun smoothScrollToDay (Lcom/kizitonwose/calendar/core/CalendarDay;)V
+ public final fun smoothScrollToMonth (Ljava/time/YearMonth;)V
+ public final fun smoothScrollToYear (Ljava/time/Year;)V
+ public final fun updateYearData ()V
+ public final fun updateYearData (Ljava/time/Year;)V
+ public final fun updateYearData (Ljava/time/Year;Ljava/time/Year;)V
+ public final fun updateYearData (Ljava/time/Year;Ljava/time/Year;Ljava/time/DayOfWeek;)V
+ public static synthetic fun updateYearData$default (Lcom/kizitonwose/calendar/view/YearCalendarView;Ljava/time/Year;Ljava/time/Year;Ljava/time/DayOfWeek;ILjava/lang/Object;)V
+}
+
+public abstract interface class com/kizitonwose/calendar/view/YearHeaderFooterBinder : com/kizitonwose/calendar/view/Binder {
+}
+
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/Binder.kt b/view/src/main/java/com/kizitonwose/calendar/view/Binder.kt
index f47504be..3cdec155 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/Binder.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/Binder.kt
@@ -3,6 +3,7 @@ package com.kizitonwose.calendar.view
import android.view.View
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.core.CalendarYear
import com.kizitonwose.calendar.core.Week
import com.kizitonwose.calendar.core.WeekDay
@@ -19,14 +20,18 @@ public interface Binder {
public fun bind(container: Container, data: Data)
}
-public interface WeekDayBinder : Binder
+public interface YearHeaderFooterBinder : Binder
-public interface WeekHeaderFooterBinder : Binder
-
-public interface MonthDayBinder : Binder
+public typealias YearScrollListener = (CalendarYear) -> Unit
public interface MonthHeaderFooterBinder : Binder
+public interface MonthDayBinder : Binder
+
public typealias MonthScrollListener = (CalendarMonth) -> Unit
+public interface WeekHeaderFooterBinder : Binder
+
+public interface WeekDayBinder : Binder
+
public typealias WeekScrollListener = (Week) -> Unit
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt
index 1769ad55..1810d1c7 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt
@@ -10,15 +10,24 @@ import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.CalendarMonth
import com.kizitonwose.calendar.core.DayPosition
import com.kizitonwose.calendar.core.OutDateStyle
+import com.kizitonwose.calendar.core.daysOfWeek
+import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
import com.kizitonwose.calendar.data.checkRange
import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelper
import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelperLegacy
+import com.kizitonwose.calendar.view.internal.missingField
import com.kizitonwose.calendar.view.internal.monthcalendar.MonthCalendarAdapter
import com.kizitonwose.calendar.view.internal.monthcalendar.MonthCalendarLayoutManager
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
+/**
+ * A month-based calendar view.
+ *
+ * @see WeekCalendarView
+ * @see YearCalendarView
+ */
public open class CalendarView : RecyclerView {
/**
* The [MonthDayBinder] instance used for managing day
@@ -31,8 +40,8 @@ public open class CalendarView : RecyclerView {
}
/**
- * The [MonthHeaderFooterBinder] instance used for managing header views.
- * The header view is shown above each month on the Calendar.
+ * The [MonthHeaderFooterBinder] instance used for managing the
+ * header views shown above each month on the calendar.
*/
public var monthHeaderBinder: MonthHeaderFooterBinder<*>? = null
set(value) {
@@ -41,8 +50,8 @@ public open class CalendarView : RecyclerView {
}
/**
- * The [MonthHeaderFooterBinder] instance used for managing footer views.
- * The footer view is shown below each month on the Calendar.
+ * The [MonthHeaderFooterBinder] instance used for managing the
+ * footer views shown below each month on the calendar.
*/
public var monthFooterBinder: MonthHeaderFooterBinder<*>? = null
set(value) {
@@ -70,7 +79,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * The xml resource that is inflated and used as a header for every month.
+ * The xml resource that is inflated and used as a header for each month.
* Set zero to disable.
*/
public var monthHeaderResource: Int = 0
@@ -82,7 +91,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * The xml resource that is inflated and used as a footer for every month.
+ * The xml resource that is inflated and used as a footer for each month.
* Set zero to disable.
*/
public var monthFooterResource: Int = 0
@@ -94,8 +103,9 @@ public open class CalendarView : RecyclerView {
}
/**
- * A [ViewGroup] which is instantiated and used as the container for each month.
- * This class must have a constructor which takes only a [Context].
+ * The fully qualified class name of a [ViewGroup] that is instantiated
+ * and used as the container for each month. This class must have a
+ * constructor which takes only a [Context].
*
* **You should exclude the name and constructor of this class from code
* obfuscation if enabled**.
@@ -151,7 +161,7 @@ public open class CalendarView : RecyclerView {
/**
* Determines how the size of each day on the calendar is calculated.
- * Can be [DaySize.Square], [DaySize.SeventhWidth] or [DaySize.FreeForm].
+ * See the [DaySize] class documentation to understand each value.
*/
public var daySize: DaySize = DaySize.Square
set(value) {
@@ -165,7 +175,7 @@ public open class CalendarView : RecyclerView {
* The margins, in pixels to be applied on each month view.
* this can be used to add a space between two months.
*/
- public var monthMargins: MarginValues = MarginValues()
+ public var monthMargins: MarginValues = MarginValues.ZERO
set(value) {
if (field != value) {
field = value
@@ -330,7 +340,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * Notify the CalendarView to reload the cell for this [CalendarDay]
+ * Notify the calendar to reload the cell for this [CalendarDay]
* This causes [MonthDayBinder.bind] to be called with the [ViewContainer]
* at this position. Use this to reload a date cell on the Calendar.
*/
@@ -353,7 +363,7 @@ public open class CalendarView : RecyclerView {
// May consider removing the other one at some point.
/**
- * Notify the CalendarView to reload the cells for this [LocalDate] in the
+ * Notify the calendar to reload the cells for this [LocalDate] in the
* specified day positions. This causes [MonthDayBinder.bind] to be called
* with the [ViewContainer] at the relevant [DayPosition] values.
*/
@@ -369,18 +379,19 @@ public open class CalendarView : RecyclerView {
}
/**
- * Notify the CalendarView to reload the view for this [YearMonth]
+ * Notify the calendar to reload the view for this [month].
+ *
* This causes the following sequence of events:
- * [MonthDayBinder.bind] will be called for all dates in this month.
- * [MonthHeaderFooterBinder.bind] will be called for this month's header view if available.
- * [MonthHeaderFooterBinder.bind] will be called for this month's footer view if available.
+ * - [MonthHeaderFooterBinder.bind] will be called for this month's header view if available.
+ * - [MonthDayBinder.bind] will be called for all dates in this month.
+ * - [MonthHeaderFooterBinder.bind] will be called for this month's footer view if available.
*/
public fun notifyMonthChanged(month: YearMonth) {
calendarAdapter.reloadMonth(month)
}
/**
- * Notify the CalendarView to reload all months.
+ * Notify the calendar to reload all months.
* @see [notifyMonthChanged].
*/
public fun notifyCalendarChanged() {
@@ -388,7 +399,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * Find the first visible month on the CalendarView.
+ * Find the first visible month on the calendar.
*
* @return The first visible month or null if not found.
*/
@@ -397,7 +408,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * Find the last visible month on the CalendarView.
+ * Find the last visible month on the calendar.
*
* @return The last visible month or null if not found.
*/
@@ -406,7 +417,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * Find the first visible day on the CalendarView.
+ * Find the first visible day on the calendar.
* This is the day at the top-left corner of the calendar.
*
* @return The first visible day or null if not found.
@@ -416,7 +427,7 @@ public open class CalendarView : RecyclerView {
}
/**
- * Find the last visible day on the CalendarView.
+ * Find the last visible day on the calendar.
* This is the day at the bottom-right corner of the calendar.
*
* @return The last visible day or null if not found.
@@ -426,15 +437,19 @@ public open class CalendarView : RecyclerView {
}
/**
- * Setup the CalendarView.
+ * Setup the calendar.
* See [updateMonthData] to update these values.
*
* @param startMonth The first month on the calendar.
* @param endMonth The last month on the calendar.
* @param firstDayOfWeek A [DayOfWeek] to be the first day of week.
+ *
+ * @see [daysOfWeek]
+ * @see [firstDayOfWeekFromLocale]
*/
public fun setup(startMonth: YearMonth, endMonth: YearMonth, firstDayOfWeek: DayOfWeek) {
checkRange(start = startMonth, end = endMonth)
+
this.startMonth = startMonth
this.endMonth = endMonth
this.firstDayOfWeek = firstDayOfWeek
@@ -453,8 +468,8 @@ public open class CalendarView : RecyclerView {
}
/**
- * Update the CalendarView's start month or end month or the first day of week.
- * This can be called only if you have called [setup] in the past.
+ * Update the calendar's start month or end month or the first day of week.
+ * This can be called only if you have previously called [setup].
* The calendar can handle really large date ranges so you may want to setup
* the calendar with a large date range instead of updating the range frequently.
*/
@@ -480,13 +495,9 @@ public open class CalendarView : RecyclerView {
)
}
- private fun requireStartMonth(): YearMonth = startMonth ?: throw getFieldException("startMonth")
-
- private fun requireEndMonth(): YearMonth = endMonth ?: throw getFieldException("endMonth")
+ private fun requireStartMonth(): YearMonth = checkNotNull(startMonth) { missingField("startMonth") }
- private fun requireFirstDayOfWeek(): DayOfWeek =
- firstDayOfWeek ?: throw getFieldException("firstDayOfWeek")
+ private fun requireEndMonth(): YearMonth = checkNotNull(endMonth) { missingField("endMonth") }
- private fun getFieldException(field: String) =
- IllegalStateException("`$field` is not set. Have you called `setup()`?")
+ private fun requireFirstDayOfWeek(): DayOfWeek = checkNotNull(firstDayOfWeek) { missingField("firstDayOfWeek") }
}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/DaySize.kt b/view/src/main/java/com/kizitonwose/calendar/view/DaySize.kt
index 11746da8..21d65db8 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/DaySize.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/DaySize.kt
@@ -4,17 +4,20 @@ import android.view.ViewGroup
/**
* Determines how the size of each day on the calendar is calculated.
+ *
+ * These values work independently in the [CalendarView] and [WeekCalendarView] classes.
+ * However, for the [YearCalendarView] class, these values work together with the [MonthHeight].
*/
public enum class DaySize {
/**
* Each day will have both width and height matching
- * the width of the calendar divided by 7.
+ * the width of the calendar month/week divided by 7.
*/
Square,
/**
* Each day will have its width matching the width of the
- * calendar divided by 7, and its height matching the
+ * calendar month/week divided by 7, and its height matching the
* height of the calendar divided by the number of weeks
* in the index - could be 4, 5 or 6 for the month calendar,
* and 1 for the week calendar. Use this if you want each
@@ -24,8 +27,8 @@ public enum class DaySize {
/**
* Each day will have its width matching the width of
- * the calendar divided by 7. This day is allowed to
- * determine its height by setting a specific value
+ * the calendar month/week divided by 7. This day is allowed
+ * to determine its height by setting a specific value
* or using [ViewGroup.LayoutParams.WRAP_CONTENT].
*/
SeventhWidth,
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/MarginValues.kt b/view/src/main/java/com/kizitonwose/calendar/view/MarginValues.kt
index fc1d7d5d..877c578f 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/MarginValues.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/MarginValues.kt
@@ -7,4 +7,18 @@ public data class MarginValues(
@Px val top: Int = 0,
@Px val end: Int = 0,
@Px val bottom: Int = 0,
-)
+) {
+ public constructor(
+ @Px horizontal: Int = 0,
+ @Px vertical: Int = 0,
+ ) : this(
+ start = horizontal,
+ top = vertical,
+ end = horizontal,
+ bottom = vertical,
+ )
+
+ public companion object {
+ public val ZERO: MarginValues = MarginValues()
+ }
+}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt
new file mode 100644
index 00000000..b572998e
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt
@@ -0,0 +1,33 @@
+package com.kizitonwose.calendar.view
+
+/**
+ * Determines how the height of each month row on the year-based
+ * calendar is calculated.
+ *
+ * **This class is only relevant for [YearCalendarView].**
+ */
+public enum class MonthHeight {
+ /**
+ * Each month row height is determined by the [DaySize] value set on the calendar.
+ * Effectively, this is `wrap-content` if the value is [DaySize.Square],
+ * [DaySize.SeventhWidth], or [DaySize.FreeForm], and will be equal to the calendar height
+ * divided by the number of rows if the value is [DaySize.Rectangle].
+ *
+ * When used together with [DaySize.Rectangle], the calendar months and days will
+ * uniformly stretch to fill the parent's height.
+ */
+ FollowDaySize,
+
+ /**
+ * Each month row height will be the calender height divided by the number
+ * of rows on the calendar. This means that the calendar months will be distributed
+ * uniformly to fill the parent's height. However, the day content height will
+ * independently determine its height.
+ *
+ * This allows you to spread the calendar months evenly across the screen while
+ * using a [DaySize] value of [DaySize.Square] if you want square day content
+ * or [DaySize.SeventhWidth] if you want to set a specific height value for
+ * the day content.
+ */
+ Fill,
+}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt
index 7e461c1f..578b5629 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt
@@ -7,13 +7,22 @@ import androidx.core.content.withStyledAttributes
import androidx.recyclerview.widget.RecyclerView
import com.kizitonwose.calendar.core.Week
import com.kizitonwose.calendar.core.WeekDay
+import com.kizitonwose.calendar.core.daysOfWeek
+import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
import com.kizitonwose.calendar.data.checkRange
import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelperLegacy
+import com.kizitonwose.calendar.view.internal.missingField
import com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarAdapter
import com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarLayoutManager
import java.time.DayOfWeek
import java.time.LocalDate
+/**
+ * A week-based calendar view.
+ *
+ * @see CalendarView
+ * @see YearCalendarView
+ */
public open class WeekCalendarView : RecyclerView {
/**
* The [WeekDayBinder] instance used for managing day
@@ -26,8 +35,8 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * The [WeekHeaderFooterBinder] instance used for managing header views.
- * The header view is shown above each week on the Calendar.
+ * The [WeekHeaderFooterBinder] instance used for managing the
+ * header views shown above each week on the calendar.
*/
public var weekHeaderBinder: WeekHeaderFooterBinder<*>? = null
set(value) {
@@ -36,8 +45,8 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * The [WeekHeaderFooterBinder] instance used for managing footer views.
- * The footer view is shown below each week on the Calendar.
+ * The [WeekHeaderFooterBinder] instance used for managing the
+ * footer views shown below each week on the calendar.
*/
public var weekFooterBinder: WeekHeaderFooterBinder<*>? = null
set(value) {
@@ -65,7 +74,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * The xml resource that is inflated and used as a header for every week.
+ * The xml resource that is inflated and used as a header for each week.
* Set zero to disable.
*/
public var weekHeaderResource: Int = 0
@@ -77,7 +86,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * The xml resource that is inflated and used as a footer for every week.
+ * The xml resource that is inflated and used as a footer for each week.
* Set zero to disable.
*/
public var weekFooterResource: Int = 0
@@ -89,8 +98,9 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * A [ViewGroup] which is instantiated and used as the container for each week.
- * This class must have a constructor which takes only a [Context].
+ * The fully qualified class name of a [ViewGroup] that is instantiated
+ * and used as the container for each week. This class must have a
+ * constructor which takes only a [Context].
*
* **You should exclude the name and constructor of this class from code
* obfuscation if enabled**.
@@ -118,7 +128,7 @@ public open class WeekCalendarView : RecyclerView {
/**
* Determines how the size of each day on the calendar is calculated.
- * Can be [DaySize.Square], [DaySize.SeventhWidth] or [DaySize.FreeForm].
+ * See the [DaySize] class documentation to understand each value.
*/
public var daySize: DaySize = DaySize.Square
set(value) {
@@ -132,7 +142,7 @@ public open class WeekCalendarView : RecyclerView {
* The margins, in pixels to be applied each week view.
* this can be used to add a space between two weeks.
*/
- public var weekMargins: MarginValues = MarginValues()
+ public var weekMargins: MarginValues = MarginValues.ZERO
set(value) {
if (field != value) {
field = value
@@ -292,7 +302,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Notify the WeekCalendarView to reload the cell for this [date].
+ * Notify the calendar to reload the cell for this [date].
* This causes [WeekDayBinder.bind] to be called with the [ViewContainer]
* at this position. Use this to reload a date cell on the Calendar.
*/
@@ -301,7 +311,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Notify the WeekCalendarView to reload the cell for this [WeekDay].
+ * Notify the calendar to reload the cell for this [WeekDay].
* This causes [WeekDayBinder.bind] to be called with the [ViewContainer]
* at this position. Use this to reload a date cell on the Calendar.
*/
@@ -310,29 +320,31 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Notify the WeekCalendarView to reload the view for the week containing
- * this [date]. This causes the following sequence of events:
- * [WeekDayBinder.bind] will be called for all dates in the week.
- * [WeekHeaderFooterBinder.bind] will be called for this week's header view if available.
- * [WeekHeaderFooterBinder.bind] will be called for this week's footer view if available.
+ * Notify the calendar to reload the view for the week containing this [date].
+ *
+ * This causes the following sequence of events:
+ * - [WeekHeaderFooterBinder.bind] will be called for this week's header view if available.
+ * - [WeekDayBinder.bind] will be called for all dates in the week.
+ * - [WeekHeaderFooterBinder.bind] will be called for this week's footer view if available.
*/
public fun notifyWeekChanged(date: LocalDate) {
calendarAdapter.reloadWeek(date)
}
/**
- * Notify the WeekCalendarView to reload the view for the week containing
- * this [WeekDay]. This causes the following sequence of events:
- * [WeekDayBinder.bind] will be called for all dates in the week.
- * [WeekHeaderFooterBinder.bind] will be called for this week's header view if available.
- * [WeekHeaderFooterBinder.bind] will be called for this week's footer view if available.
+ * Notify the calendar to reload the view for the week containing this [WeekDay].
+ *
+ * This causes the following sequence of events:
+ * - [WeekHeaderFooterBinder.bind] will be called for this week's header view if available.
+ * - [WeekDayBinder.bind] will be called for all dates in the week.
+ * - [WeekHeaderFooterBinder.bind] will be called for this week's footer view if available.
*/
public fun notifyWeekChanged(day: WeekDay) {
notifyWeekChanged(day.date)
}
/**
- * Notify the WeekCalendarView to reload all weeks.
+ * Notify the calendar to reload all weeks.
*
* @see [notifyWeekChanged]
*/
@@ -341,7 +353,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Find the first visible week on the WeekCalendarView.
+ * Find the first visible week on the calendar.
*
* @return The first visible week or null if not found.
*/
@@ -350,7 +362,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Find the last visible week on the WeekCalendarView.
+ * Find the last visible week on the calendar.
*
* @return The last visible week or null if not found.
*/
@@ -359,7 +371,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Find the first visible day on the WeekCalendarView.
+ * Find the first visible day on the calendar.
* This is the day at the top-left corner of the calendar.
*
* @return The first visible day or null if not found.
@@ -369,7 +381,7 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Find the last visible day on the WeekCalendarView.
+ * Find the last visible day on the calendar.
* This is the day at the bottom-right corner of the calendar.
*
* @return The last visible day or null if not found.
@@ -379,12 +391,15 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Setup the WeekCalendarView.
+ * Setup the calendar.
* See [updateWeekData] to update these values.
*
* @param startDate A date in the first week on the calendar.
* @param endDate A date in the last week on the calendar.
* @param firstDayOfWeek A [DayOfWeek] to be the first day of week.
+ *
+ * @see [daysOfWeek]
+ * @see [firstDayOfWeekFromLocale]
*/
public fun setup(startDate: LocalDate, endDate: LocalDate, firstDayOfWeek: DayOfWeek) {
checkRange(start = startDate, end = endDate)
@@ -405,8 +420,8 @@ public open class WeekCalendarView : RecyclerView {
}
/**
- * Update the WeekCalendarView's start date or end date or the first day of week.
- * This can be called only if you have called [setup] in the past.
+ * Update the calendar's start date or end date or the first day of week.
+ * This can be called only if you have previously called [setup].
* The calendar can handle really large date ranges so you may want to setup
* the calendar with a large date range instead of updating the range frequently.
*/
@@ -431,13 +446,9 @@ public open class WeekCalendarView : RecyclerView {
)
}
- private fun requireStartDate(): LocalDate = startDate ?: throw getFieldException("startDate")
-
- private fun requireEndDate(): LocalDate = endDate ?: throw getFieldException("endDate")
+ private fun requireStartDate(): LocalDate = checkNotNull(startDate) { missingField("startDate") }
- private fun requireFirstDayOfWeek(): DayOfWeek =
- firstDayOfWeek ?: throw getFieldException("firstDayOfWeek")
+ private fun requireEndDate(): LocalDate = checkNotNull(endDate) { missingField("endDate") }
- private fun getFieldException(field: String) =
- IllegalStateException("`$field` is not set. Have you called `setup()`?")
+ private fun requireFirstDayOfWeek(): DayOfWeek = checkNotNull(firstDayOfWeek) { missingField("firstDayOfWeek") }
}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt
new file mode 100644
index 00000000..a40f4588
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt
@@ -0,0 +1,712 @@
+package com.kizitonwose.calendar.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import androidx.annotation.IntRange
+import androidx.annotation.Px
+import androidx.core.content.withStyledAttributes
+import androidx.recyclerview.widget.PagerSnapHelper
+import androidx.recyclerview.widget.RecyclerView
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.core.CalendarYear
+import com.kizitonwose.calendar.core.DayPosition
+import com.kizitonwose.calendar.core.OutDateStyle
+import com.kizitonwose.calendar.core.daysOfWeek
+import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
+import com.kizitonwose.calendar.data.checkRange
+import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelper
+import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelperLegacy
+import com.kizitonwose.calendar.view.internal.missingField
+import com.kizitonwose.calendar.view.internal.yearcalendar.YearCalendarAdapter
+import com.kizitonwose.calendar.view.internal.yearcalendar.YearCalendarLayoutManager
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.Year
+import java.time.YearMonth
+
+/**
+ * A year-based calendar view.
+ *
+ * @see CalendarView
+ * @see WeekCalendarView
+ */
+public open class YearCalendarView : RecyclerView {
+ /**
+ * The [MonthDayBinder] instance used for managing day
+ * cell view creation and reuse on the calendar.
+ */
+ public var dayBinder: MonthDayBinder<*>? = null
+ set(value) {
+ field = value
+ invalidateViewHolders()
+ }
+
+ /**
+ * The [MonthHeaderFooterBinder] instance used for managing the
+ * header views shown above each month on the calendar.
+ */
+ public var monthHeaderBinder: MonthHeaderFooterBinder<*>? = null
+ set(value) {
+ field = value
+ invalidateViewHolders()
+ }
+
+ /**
+ * The [MonthHeaderFooterBinder] instance used for managing the
+ * footer views shown below each month on the calendar.
+ */
+ public var monthFooterBinder: MonthHeaderFooterBinder<*>? = null
+ set(value) {
+ field = value
+ invalidateViewHolders()
+ }
+
+ /**
+ * The [YearHeaderFooterBinder] instance used for managing the
+ * header views shown above each year on the calendar.
+ */
+ public var yearHeaderBinder: YearHeaderFooterBinder<*>? = null
+ set(value) {
+ field = value
+ invalidateViewHolders()
+ }
+
+ /**
+ * The [YearHeaderFooterBinder] instance used for managing the
+ * footer views shown below each year on the calendar.
+ */
+ public var yearFooterBinder: YearHeaderFooterBinder<*>? = null
+ set(value) {
+ field = value
+ invalidateViewHolders()
+ }
+
+ /**
+ * Called when the calendar scrolls to a new year.
+ * Mostly beneficial if [scrollPaged] is `true`.
+ */
+ public var yearScrollListener: YearScrollListener? = null
+
+ /**
+ * The xml resource that is inflated and used as the day cell view.
+ * This must be provided.
+ */
+ public var dayViewResource: Int = 0
+ set(value) {
+ if (field != value) {
+ require(value != 0) { "Invalid 'dayViewResource' value." }
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The xml resource that is inflated and used as a header for each month.
+ * Set zero to disable.
+ */
+ public var monthHeaderResource: Int = 0
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The xml resource that is inflated and used as a footer for each month.
+ * Set zero to disable.
+ */
+ public var monthFooterResource: Int = 0
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The xml resource that is inflated and used as a header for each year.
+ * Set zero to disable.
+ */
+ public var yearHeaderResource: Int = 0
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The xml resource that is inflated and used as a footer for each year.
+ * Set zero to disable.
+ */
+ public var yearFooterResource: Int = 0
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The fully qualified class name of a [ViewGroup] that is instantiated
+ * and used as the container for each month. This class must have a
+ * constructor which takes only a [Context].
+ *
+ * **You should exclude the name and constructor of this class from code
+ * obfuscation if enabled**.
+ */
+ public var monthViewClass: String? = null
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The fully qualified class name of a [ViewGroup] that is instantiated
+ * and used as the container for each year. This class must have a
+ * constructor which takes only a [Context].
+ *
+ * **You should exclude the name and constructor of this class from code
+ * obfuscation if enabled**.
+ */
+ public var yearViewClass: String? = null
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The vertical spacing between month rows in each year.
+ */
+ @Px
+ public var monthVerticalSpacing: Int = 0
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The horizontal spacing between month columns in each year.
+ */
+ @Px
+ public var monthHorizontalSpacing: Int = 0
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The number of month columns in each year. Must be from 1 to 12.
+ */
+ @IntRange(from = 1, to = 12)
+ public var monthColumns: Int = 3
+ set(value) {
+ if (field != value) {
+ require(value in 1..12) { "Month columns must be 1..12" }
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * Determines if a month is shown on the calendar grid. For example, you can
+ * use this to hide all past months.
+ */
+ public var isMonthVisible: (month: CalendarMonth) -> Boolean = { true }
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The [RecyclerView.Orientation] used for the layout manager.
+ * This determines the scroll direction of the calendar.
+ */
+ @Orientation
+ public var orientation: Int = HORIZONTAL
+ set(value) {
+ if (field != value) {
+ field = value
+ (layoutManager as? YearCalendarLayoutManager)?.orientation = value
+ updateSnapHelper()
+ }
+ }
+
+ /**
+ * The scrolling behavior of the calendar. If `true`, the calendar will
+ * snap to the nearest year after a scroll or swipe action.
+ * If `false`, the calendar scrolls normally.
+ */
+ public var scrollPaged: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ updateSnapHelper()
+ }
+ }
+
+ /**
+ * Determines how outDates are generated for each month on the calendar.
+ * Can be [OutDateStyle.EndOfRow] or [OutDateStyle.EndOfGrid].
+ *
+ * @see [DayPosition]
+ */
+ public var outDateStyle: OutDateStyle = OutDateStyle.EndOfRow
+ set(value) {
+ if (field != value) {
+ field = value
+ if (adapter != null) updateAdapter()
+ }
+ }
+
+ /**
+ * Determines how the size of each day on the calendar is calculated.
+ * See the [DaySize] class documentation to understand each value.
+ *
+ * @see [monthHeight]
+ */
+ public var daySize: DaySize = DaySize.Square
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * Determines how the height of each month on the calendar is calculated.
+ * See the [MonthHeight] class documentation to understand each value.
+ *
+ * @see [daySize]
+ */
+ public var monthHeight: MonthHeight = MonthHeight.FollowDaySize
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The margins, in pixels to be applied on each year view.
+ * This is the container in which the year header, body and footer are placed.
+ * For example, this can be used to add a space between two years.
+ */
+ public var yearMargins: MarginValues = MarginValues.ZERO
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ /**
+ * The margins, in pixels to be applied on each year body view.
+ * This is the grid in which the months in each year are shown,
+ * excluding the year header and footer.
+ */
+ public var yearBodyMargins: MarginValues = MarginValues.ZERO
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateViewHolders()
+ }
+ }
+
+ private val scrollListenerInternal = object : OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {}
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ if (newState == SCROLL_STATE_IDLE) {
+ calendarAdapter.notifyYearScrollListenerIfNeeded()
+ }
+ }
+ }
+
+ private val horizontalSnapHelper = CalendarPageSnapHelperLegacy()
+ private val verticalSnapHelper = CalendarPageSnapHelper()
+ private var pageSnapHelper: PagerSnapHelper = horizontalSnapHelper
+
+ private var startYear: Year? = null
+ private var endYear: Year? = null
+ private var firstDayOfWeek: DayOfWeek? = null
+
+ public constructor(context: Context) : super(context)
+
+ public constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ init(attrs, 0, 0)
+ }
+
+ public constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
+ super(context, attrs, defStyleAttr) {
+ init(attrs, defStyleAttr, defStyleAttr)
+ }
+
+ private fun init(attributeSet: AttributeSet, defStyleAttr: Int, defStyleRes: Int) {
+ if (isInEditMode) return
+ itemAnimator = null
+ setHasFixedSize(true)
+ context.withStyledAttributes(
+ attributeSet,
+ R.styleable.YearCalendarView,
+ defStyleAttr,
+ defStyleRes,
+ ) {
+ dayViewResource = getResourceId(
+ R.styleable.YearCalendarView_cv_dayViewResource,
+ dayViewResource,
+ )
+ monthHeaderResource = getResourceId(
+ R.styleable.YearCalendarView_cv_monthHeaderResource,
+ monthHeaderResource,
+ )
+ monthFooterResource = getResourceId(
+ R.styleable.YearCalendarView_cv_monthFooterResource,
+ monthFooterResource,
+ )
+ yearHeaderResource = getResourceId(
+ R.styleable.YearCalendarView_cv_yearHeaderResource,
+ yearHeaderResource,
+ )
+ yearFooterResource = getResourceId(
+ R.styleable.YearCalendarView_cv_yearFooterResource,
+ yearFooterResource,
+ )
+ orientation = getInt(R.styleable.YearCalendarView_cv_orientation, orientation)
+ // Enable paged scrolling by default only for the horizontal calendar.
+ scrollPaged = getBoolean(
+ R.styleable.YearCalendarView_cv_scrollPaged,
+ orientation == HORIZONTAL,
+ )
+ daySize = DaySize.entries[
+ getInt(R.styleable.YearCalendarView_cv_daySize, daySize.ordinal),
+ ]
+ monthHeight = MonthHeight.entries[
+ getInt(R.styleable.YearCalendarView_cv_monthHeight, monthHeight.ordinal),
+ ]
+ outDateStyle = OutDateStyle.entries[
+ getInt(R.styleable.YearCalendarView_cv_outDateStyle, outDateStyle.ordinal),
+ ]
+ monthColumns = getInt(
+ R.styleable.YearCalendarView_cv_monthColumns,
+ monthColumns,
+ )
+ monthHorizontalSpacing = getDimensionPixelSize(
+ R.styleable.YearCalendarView_cv_monthHorizontalSpacing,
+ monthHorizontalSpacing,
+ )
+ monthVerticalSpacing = getDimensionPixelSize(
+ R.styleable.YearCalendarView_cv_monthVerticalSpacing,
+ monthVerticalSpacing,
+ )
+ monthViewClass = getString(R.styleable.YearCalendarView_cv_monthViewClass)
+ yearViewClass = getString(R.styleable.YearCalendarView_cv_yearViewClass)
+ }
+ check(dayViewResource != 0) { "No value set for `cv_dayViewResource` attribute." }
+ }
+
+ private val calendarLayoutManager: YearCalendarLayoutManager
+ get() = layoutManager as YearCalendarLayoutManager
+
+ private val calendarAdapter: YearCalendarAdapter
+ get() = adapter as YearCalendarAdapter
+
+ private fun invalidateViewHolders() {
+ // This does not remove visible views.
+ // recycledViewPool.clear()
+
+ // This removes all views but is internal.
+ // removeAndRecycleViews()
+
+ if (adapter == null || layoutManager == null) return
+ val state = layoutManager?.onSaveInstanceState()
+ adapter = adapter
+ layoutManager?.onRestoreInstanceState(state)
+ post { calendarAdapter.notifyYearScrollListenerIfNeeded() }
+ }
+
+ private fun updateSnapHelper() {
+ if (!scrollPaged) {
+ pageSnapHelper.attachToRecyclerView(null)
+ return
+ }
+ if (
+ (orientation == HORIZONTAL && pageSnapHelper !== horizontalSnapHelper) ||
+ (orientation == VERTICAL && pageSnapHelper !== verticalSnapHelper)
+ ) {
+ // Remove the currently attached snap helper.
+ pageSnapHelper.attachToRecyclerView(null)
+ pageSnapHelper =
+ if (orientation == HORIZONTAL) horizontalSnapHelper else verticalSnapHelper
+ }
+ pageSnapHelper.attachToRecyclerView(this)
+ }
+
+ /**
+ * Scroll to a specific year on the calendar. This instantly
+ * shows the view for the year without any animations.
+ * For a smooth scrolling effect, use [smoothScrollToYear]
+ */
+ public fun scrollToYear(year: Year) {
+ calendarLayoutManager.scrollToIndex(year)
+ }
+
+ /**
+ * Scroll to a specific year on the calendar using a smooth scrolling animation.
+ * Just like [scrollToYear], but with a smooth scrolling animation.
+ */
+ public fun smoothScrollToYear(year: Year) {
+ calendarLayoutManager.smoothScrollToIndex(year)
+ }
+
+ /**
+ * Scroll to a specific month on the calendar. This instantly
+ * shows the view for the month without any animations.
+ * For a smooth scrolling effect, use [smoothScrollToMonth]
+ */
+ public fun scrollToMonth(month: YearMonth) {
+ calendarLayoutManager.scrollToMonth(month)
+ }
+
+ /**
+ * Scroll to a specific month on the calendar using a smooth scrolling animation.
+ * Just like [scrollToMonth], but with a smooth scrolling animation.
+ */
+ public fun smoothScrollToMonth(month: YearMonth) {
+ calendarLayoutManager.smoothScrollToMonth(month)
+ }
+
+ /**
+ * Scroll to a specific [CalendarDay]. This brings the date cell
+ * view's top to the top of the CalendarVew in vertical mode or
+ * the cell view's left edge to the left edge of the CalendarVew
+ * in horizontal mode. No animation is performed.
+ * For a smooth scrolling effect, use [smoothScrollToDay].
+ */
+ public fun scrollToDay(day: CalendarDay) {
+ calendarLayoutManager.scrollToDay(day)
+ }
+
+ /**
+ * Shortcut for [scrollToDay] with a [LocalDate] instance.
+ */
+ @JvmOverloads
+ public fun scrollToDate(date: LocalDate, position: DayPosition = DayPosition.MonthDate) {
+ scrollToDay(CalendarDay(date, position))
+ }
+
+ /**
+ * Scroll to a specific [CalendarDay] using a smooth scrolling animation.
+ * Just like [scrollToDay], but with a smooth scrolling animation.
+ */
+ public fun smoothScrollToDay(day: CalendarDay) {
+ calendarLayoutManager.smoothScrollToDay(day)
+ }
+
+ /**
+ * Shortcut for [smoothScrollToDay] with a [LocalDate] instance.
+ */
+ @JvmOverloads
+ public fun smoothScrollToDate(date: LocalDate, position: DayPosition = DayPosition.MonthDate) {
+ smoothScrollToDay(CalendarDay(date, position))
+ }
+
+ /**
+ * Notify the calendar to reload the cell for this [CalendarDay]
+ * This causes [MonthDayBinder.bind] to be called with the [ViewContainer]
+ * at this position. Use this to reload a date cell on the Calendar.
+ */
+ public fun notifyDayChanged(day: CalendarDay) {
+ calendarAdapter.reloadDay(day)
+ }
+
+ /**
+ * Shortcut for [notifyDayChanged] with a [LocalDate] instance.
+ */
+ @JvmOverloads
+ public fun notifyDateChanged(date: LocalDate, position: DayPosition = DayPosition.MonthDate) {
+ notifyDayChanged(CalendarDay(date, position))
+ }
+
+ /**
+ * Notify the calendar to reload the cells for this [LocalDate] in the
+ * specified day positions. This causes [MonthDayBinder.bind] to be called
+ * with the [ViewContainer] at the relevant [DayPosition] values.
+ */
+ public fun notifyDateChanged(
+ date: LocalDate,
+ vararg position: DayPosition,
+ ) {
+ val days = position
+ .ifEmpty { arrayOf(DayPosition.MonthDate) }
+ .map { CalendarDay(date, it) }
+ .toSet()
+ calendarAdapter.reloadDay(*days.toTypedArray())
+ }
+
+ /**
+ * Notify the calendar to reload the view for this [month].
+ *
+ * This causes the following sequence of events:
+ * - [MonthHeaderFooterBinder.bind] will be called for this month's header view if available.
+ * - [MonthDayBinder.bind] will be called for all dates in this month.
+ * - [MonthHeaderFooterBinder.bind] will be called for this month's footer view if available.
+ */
+ public fun notifyMonthChanged(month: YearMonth) {
+ calendarAdapter.reloadMonth(month)
+ }
+
+ /**
+ * Notify the calendar to reload the view for this [year].
+ *
+ * This causes the following sequence of events:
+ * - [YearHeaderFooterBinder.bind] will be called for this year's header view if available.
+ * - [MonthHeaderFooterBinder.bind] will be called for each month's header view in this year if available.
+ * - [MonthDayBinder.bind] will be called for all dates in this year.
+ * - [MonthHeaderFooterBinder.bind] will be called for each month's footer view if available.
+ * - [YearHeaderFooterBinder.bind] will be called for this year's footer view in this year if available.
+ */
+ public fun notifyYearChanged(year: Year) {
+ calendarAdapter.reloadYear(year)
+ }
+
+ /**
+ * Notify the calendar to reload all years.
+ * @see [notifyYearChanged].
+ */
+ public fun notifyCalendarChanged() {
+ calendarAdapter.reloadCalendar()
+ }
+
+ /**
+ * Find the first visible year on the calendar.
+ *
+ * @return The first visible year or null if not found.
+ */
+ public fun findFirstVisibleYear(): CalendarYear? {
+ return calendarAdapter.findFirstVisibleYear()
+ }
+
+ /**
+ * Find the last visible year on the calendar.
+ *
+ * @return The last visible year or null if not found.
+ */
+ public fun findLastVisibleYear(): CalendarYear? {
+ return calendarAdapter.findLastVisibleYear()
+ }
+
+ /**
+ * Find the first visible month on the calendar.
+ *
+ * @return The first visible month or null if not found.
+ */
+ public fun findFirstVisibleMonth(): CalendarMonth? {
+ return calendarAdapter.findFirstVisibleMonth()
+ }
+
+ /**
+ * Find the last visible month on the calendar.
+ *
+ * @return The last visible month or null if not found.
+ */
+ public fun findLastVisibleMonth(): CalendarMonth? {
+ return calendarAdapter.findLastVisibleMonth()
+ }
+
+ /**
+ * Find the first visible day on the calendar.
+ * This is the day at the top-left corner of the calendar.
+ *
+ * @return The first visible day or null if not found.
+ */
+ public fun findFirstVisibleDay(): CalendarDay? {
+ return calendarAdapter.findFirstVisibleDay()
+ }
+
+ /**
+ * Find the last visible day on the calendar.
+ * This is the day at the bottom-right corner of the calendar.
+ *
+ * @return The last visible day or null if not found.
+ */
+ public fun findLastVisibleDay(): CalendarDay? {
+ return calendarAdapter.findLastVisibleDay()
+ }
+
+ /**
+ * Setup the calendar.
+ * See [updateYearData] to update these values.
+ *
+ * @param startYear The first year on the calendar.
+ * @param endYear The last year on the calendar.
+ * @param firstDayOfWeek A [DayOfWeek] to be the first day of week.
+ *
+ * @see [daysOfWeek]
+ * @see [firstDayOfWeekFromLocale]
+ */
+ public fun setup(startYear: Year, endYear: Year, firstDayOfWeek: DayOfWeek) {
+ checkRange(start = startYear, end = endYear)
+ this.startYear = startYear
+ this.endYear = endYear
+ this.firstDayOfWeek = firstDayOfWeek
+
+ removeOnScrollListener(scrollListenerInternal)
+ addOnScrollListener(scrollListenerInternal)
+
+ layoutManager = YearCalendarLayoutManager(this)
+ adapter = YearCalendarAdapter(
+ calView = this,
+ outDateStyle = outDateStyle,
+ startYear = startYear,
+ endYear = endYear,
+ firstDayOfWeek = firstDayOfWeek,
+ )
+ }
+
+ /**
+ * Update the calendar's start year or end year or the first day of week.
+ * This can be called only if you have previously called [setup].
+ * The calendar can handle really large date ranges so you may want to setup
+ * the calendar with a large date range instead of updating the range frequently.
+ */
+ @JvmOverloads
+ public fun updateYearData(
+ startYear: Year = requireStartYear(),
+ endYear: Year = requireEndYear(),
+ firstDayOfWeek: DayOfWeek = requireFirstDayOfWeek(),
+ ) {
+ checkRange(start = startYear, end = endYear)
+ this.startYear = startYear
+ this.endYear = endYear
+ this.firstDayOfWeek = firstDayOfWeek
+ updateAdapter()
+ }
+
+ private fun updateAdapter() {
+ calendarAdapter.updateData(
+ startYear = requireStartYear(),
+ endYear = requireEndYear(),
+ outDateStyle = outDateStyle,
+ firstDayOfWeek = requireFirstDayOfWeek(),
+ )
+ }
+
+ private fun requireStartYear(): Year = checkNotNull(startYear) { missingField("startYear") }
+
+ private fun requireEndYear(): Year = checkNotNull(endYear) { missingField("endYear") }
+
+ private fun requireFirstDayOfWeek(): DayOfWeek = checkNotNull(firstDayOfWeek) { missingField("firstDayOfWeek") }
+}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/CalendarLayoutManager.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/CalendarLayoutManager.kt
index 292f2d27..05d759ca 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/CalendarLayoutManager.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/CalendarLayoutManager.kt
@@ -62,9 +62,12 @@ internal abstract class CalendarLayoutManager(
val rect = Rect()
dayView.getDrawingRect(rect)
(itemView as ViewGroup).offsetDescendantRectToMyCoords(dayView, rect)
- val isVertical = orientation == VERTICAL
val margins = getItemMargins()
- return if (isVertical) rect.top + margins.top else rect.left + margins.start
+ return if (orientation == VERTICAL) {
+ rect.top + margins.top
+ } else {
+ rect.left + margins.start
+ }
}
private inner class CalendarSmoothScroller(position: Int, val day: DayData?) :
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/CustomViewClass.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/CustomViewClass.kt
new file mode 100644
index 00000000..7810c304
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/CustomViewClass.kt
@@ -0,0 +1,36 @@
+package com.kizitonwose.calendar.view.internal
+
+import android.content.Context
+import android.util.Log
+import android.view.ViewGroup
+
+internal inline fun customViewOrRoot(
+ customViewClass: String?,
+ rootLayout: ViewGroup,
+ setupRoot: (ViewGroup) -> Unit,
+): ViewGroup {
+ return customViewClass?.let {
+ val customLayout = runCatching {
+ Class.forName(it)
+ .getDeclaredConstructor(Context::class.java)
+ .newInstance(rootLayout.context) as ViewGroup
+ }.onFailure {
+ Log.e(
+ "Calendar",
+ "Failure loading custom class $customViewClass, " +
+ "check that $customViewClass is a ViewGroup and the " +
+ "single argument context constructor is available. " +
+ "For an example on how to use a custom class, see: $EXAMPLE_CUSTOM_CLASS_URL",
+ it,
+ )
+ }.getOrNull()
+
+ customLayout?.apply {
+ setupRoot(this)
+ addView(rootLayout)
+ }
+ } ?: rootLayout.apply { setupRoot(this) }
+}
+
+private const val EXAMPLE_CUSTOM_CLASS_URL =
+ "https://github.com/kizitonwose/Calendar/blob/3dfb2d2e91d5e443b540ff411113a05268e4b8d2/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example6Fragment.kt#L29"
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/DayHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/DayHolder.kt
index 4a03e2d4..502cfe5b 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/DayHolder.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/DayHolder.kt
@@ -33,13 +33,16 @@ internal class DayHolder(private val config: DayConfig) {
width = MATCH_PARENT
height = MATCH_PARENT
}
+
DaySize.Rectangle -> {
width = MATCH_PARENT
height = MATCH_PARENT
}
+
DaySize.SeventhWidth -> {
width = MATCH_PARENT
}
+
DaySize.FreeForm -> {}
}
}
@@ -51,12 +54,7 @@ internal class DayHolder(private val config: DayConfig) {
if (!::viewContainer.isInitialized) {
viewContainer = config.dayBinder.create(dayView)
}
-
- val dayTag = dayTag(findDate(currentDay))
- if (dayView.tag != dayTag) {
- dayView.tag = dayTag
- }
-
+ dayView.tag = dayTag(findDate(currentDay))
config.dayBinder.bind(viewContainer, currentDay)
}
@@ -81,6 +79,7 @@ private fun findDate(day: Any?): LocalDate {
@Suppress("FunctionName")
internal fun DayLinearLayoutParams(layoutParams: ViewGroup.LayoutParams): LinearLayout.LayoutParams =
if (layoutParams is ViewGroup.MarginLayoutParams) {
+ // Ensure we call the correct constructor.
LinearLayout.LayoutParams(layoutParams)
} else {
LinearLayout.LayoutParams(layoutParams)
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/Extensions.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/Extensions.kt
index dc427a44..3f8f0db2 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/Extensions.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/Extensions.kt
@@ -1,12 +1,37 @@
package com.kizitonwose.calendar.view.internal
+import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.DayPosition
+import com.kizitonwose.calendar.core.nextMonth
+import com.kizitonwose.calendar.core.previousMonth
+import com.kizitonwose.calendar.core.yearMonth
+import java.time.YearMonth
internal fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
}
internal const val NO_INDEX = -1
+
+// Find the actual month on the calendar where this date is shown.
+internal val CalendarDay.positionYearMonth: YearMonth
+ get() = when (position) {
+ DayPosition.InDate -> date.yearMonth.nextMonth
+ DayPosition.MonthDate -> date.yearMonth
+ DayPosition.OutDate -> date.yearMonth.previousMonth
+ }
+
+internal fun Rect.intersects(other: Rect): Boolean {
+ return if (this.isEmpty || other.isEmpty) {
+ false
+ } else {
+ Rect.intersects(this, other)
+ }
+}
+
+internal fun missingField(field: String) = "`$field` is not set. Have you called `setup()`?"
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/ItemRoot.kt
similarity index 70%
rename from view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt
rename to view/src/main/java/com/kizitonwose/calendar/view/internal/ItemRoot.kt
index f52981a6..4dc13419 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/ItemRoot.kt
@@ -1,7 +1,6 @@
package com.kizitonwose.calendar.view.internal
import android.content.Context
-import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@@ -68,7 +67,10 @@ internal fun setupItemRoot(
null
}
- fun setupRoot(root: ViewGroup) {
+ val itemView = customViewOrRoot(
+ customViewClass = itemViewClass,
+ rootLayout = rootLayout,
+ ) { root: ViewGroup ->
val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT
val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT
root.layoutParams = MarginLayoutParams(width, height).apply {
@@ -79,28 +81,6 @@ internal fun setupItemRoot(
}
}
- val itemView = itemViewClass?.let {
- val customLayout = runCatching {
- Class.forName(it)
- .getDeclaredConstructor(Context::class.java)
- .newInstance(rootLayout.context) as ViewGroup
- }.onFailure {
- Log.e(
- "CalendarView",
- "Failure loading custom class $itemViewClass, " +
- "check that $itemViewClass is a ViewGroup and the " +
- "single argument context constructor is available. " +
- "For an example on how to use a custom class, see: $EXAMPLE_CUSTOM_CLASS_URL",
- it,
- )
- }.getOrNull()
-
- customLayout?.apply {
- setupRoot(this)
- addView(rootLayout)
- }
- } ?: rootLayout.apply { setupRoot(this) }
-
return ItemContent(
itemView = itemView,
headerView = itemHeaderView,
@@ -110,5 +90,3 @@ internal fun setupItemRoot(
}
internal fun dayTag(date: LocalDate): Int = date.hashCode()
-private const val EXAMPLE_CUSTOM_CLASS_URL =
- "https://github.com/kizitonwose/Calendar/blob/3dfb2d2e91d5e443b540ff411113a05268e4b8d2/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example6Fragment.kt#L29"
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthCalendarAdapter.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthCalendarAdapter.kt
index 46408d0c..2e38b43f 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthCalendarAdapter.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthCalendarAdapter.kt
@@ -8,11 +8,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.recyclerview.widget.RecyclerView
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.CalendarMonth
-import com.kizitonwose.calendar.core.DayPosition
import com.kizitonwose.calendar.core.OutDateStyle
-import com.kizitonwose.calendar.core.nextMonth
-import com.kizitonwose.calendar.core.previousMonth
-import com.kizitonwose.calendar.core.yearMonth
import com.kizitonwose.calendar.data.DataStore
import com.kizitonwose.calendar.data.getCalendarMonthData
import com.kizitonwose.calendar.data.getMonthIndex
@@ -23,6 +19,8 @@ import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
import com.kizitonwose.calendar.view.ViewContainer
import com.kizitonwose.calendar.view.internal.NO_INDEX
import com.kizitonwose.calendar.view.internal.dayTag
+import com.kizitonwose.calendar.view.internal.intersects
+import com.kizitonwose.calendar.view.internal.positionYearMonth
import com.kizitonwose.calendar.view.internal.setupItemRoot
import java.time.DayOfWeek
import java.time.YearMonth
@@ -184,14 +182,30 @@ internal class MonthCalendarAdapter(
private fun findLastVisibleMonthPosition(): Int = layoutManager.findLastVisibleItemPosition()
+ /**
+ * In a vertically scrolling calendar, month headers/footers can cause the visible
+ * day rect to not be found in the returned visible month index from a call to
+ * findFirstVisibleItemPosition/findLastVisibleItemPosition if only the header
+ * or footer of the month in that index is visible. So we check adjacent indices too.
+ */
private fun findVisibleDay(isFirst: Boolean): CalendarDay? {
- val visibleIndex =
- if (isFirst) findFirstVisibleMonthPosition() else findLastVisibleMonthPosition()
+ return visibleDay(isFirst)
+ ?: visibleDay(isFirst, monthIncrement = -1)
+ ?: visibleDay(isFirst, monthIncrement = 1)
+ }
+
+ private fun visibleDay(isFirst: Boolean, monthIncrement: Int = 0): CalendarDay? {
+ var visibleIndex = if (isFirst) {
+ findFirstVisibleMonthPosition()
+ } else {
+ findLastVisibleMonthPosition()
+ }
if (visibleIndex == NO_INDEX) return null
+ visibleIndex += monthIncrement
val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null
val monthRect = Rect()
- visibleItemView.getGlobalVisibleRect(monthRect)
+ if (!visibleItemView.getGlobalVisibleRect(monthRect) || monthRect.isEmpty) return null
val dayRect = Rect()
return dataStore[visibleIndex].weekDays.flatten()
@@ -199,8 +213,8 @@ internal class MonthCalendarAdapter(
.firstOrNull {
val dayView = visibleItemView.findViewWithTag(dayTag(it.date))
?: return@firstOrNull false
- dayView.getGlobalVisibleRect(dayRect)
- dayRect.intersect(monthRect)
+ dayView.getGlobalVisibleRect(dayRect) &&
+ dayRect.intersects(monthRect)
}
}
@@ -220,11 +234,3 @@ internal class MonthCalendarAdapter(
notifyDataSetChanged()
}
}
-
-// Find the actual month on the calendar where this date is shown.
-internal val CalendarDay.positionYearMonth: YearMonth
- get() = when (position) {
- DayPosition.InDate -> date.yearMonth.nextMonth
- DayPosition.MonthDate -> date.yearMonth
- DayPosition.OutDate -> date.yearMonth.previousMonth
- }
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthViewHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthViewHolder.kt
index be0554ea..c90cf87c 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthViewHolder.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/monthcalendar/MonthViewHolder.kt
@@ -14,13 +14,12 @@ internal class MonthViewHolder(
private val headerView: View?,
private val footerView: View?,
private val weekHolders: List>,
- private var monthHeaderBinder: MonthHeaderFooterBinder?,
- private var monthFooterBinder: MonthHeaderFooterBinder?,
+ private val monthHeaderBinder: MonthHeaderFooterBinder?,
+ private val monthFooterBinder: MonthHeaderFooterBinder?,
) : RecyclerView.ViewHolder(rootLayout) {
private var headerContainer: ViewContainer? = null
private var footerContainer: ViewContainer? = null
-
- lateinit var month: CalendarMonth
+ private lateinit var month: CalendarMonth
fun bindMonth(month: CalendarMonth) {
this.month = month
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekCalendarAdapter.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekCalendarAdapter.kt
index b5f02c64..d58b1356 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekCalendarAdapter.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekCalendarAdapter.kt
@@ -18,6 +18,7 @@ import com.kizitonwose.calendar.view.WeekDayBinder
import com.kizitonwose.calendar.view.WeekHeaderFooterBinder
import com.kizitonwose.calendar.view.internal.NO_INDEX
import com.kizitonwose.calendar.view.internal.dayTag
+import com.kizitonwose.calendar.view.internal.intersects
import com.kizitonwose.calendar.view.internal.setupItemRoot
import java.time.DayOfWeek
import java.time.LocalDate
@@ -161,7 +162,7 @@ internal class WeekCalendarAdapter(
val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null
val weekRect = Rect()
- visibleItemView.getGlobalVisibleRect(weekRect)
+ if (!visibleItemView.getGlobalVisibleRect(weekRect) || weekRect.isEmpty) return null
val dayRect = Rect()
return dataStore[visibleIndex].days
@@ -169,8 +170,8 @@ internal class WeekCalendarAdapter(
.firstOrNull {
val dayView = visibleItemView.findViewWithTag(dayTag(it.date))
?: return@firstOrNull false
- dayView.getGlobalVisibleRect(dayRect)
- dayRect.intersect(weekRect)
+ dayView.getGlobalVisibleRect(dayRect) &&
+ dayRect.intersects(weekRect)
}
}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekViewHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekViewHolder.kt
index 8466b4d1..82fa5924 100644
--- a/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekViewHolder.kt
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/weekcalendar/WeekViewHolder.kt
@@ -14,8 +14,8 @@ internal class WeekViewHolder(
private val headerView: View?,
private val footerView: View?,
private val weekHolder: WeekHolder,
- private var weekHeaderBinder: WeekHeaderFooterBinder?,
- private var weekFooterBinder: WeekHeaderFooterBinder?,
+ private val weekHeaderBinder: WeekHeaderFooterBinder?,
+ private val weekFooterBinder: WeekHeaderFooterBinder?,
) : RecyclerView.ViewHolder(rootLayout) {
private var headerContainer: ViewContainer? = null
private var footerContainer: ViewContainer? = null
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt
new file mode 100644
index 00000000..f8f8c818
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt
@@ -0,0 +1,290 @@
+package com.kizitonwose.calendar.view.internal.yearcalendar
+
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.recyclerview.widget.RecyclerView
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.core.CalendarYear
+import com.kizitonwose.calendar.core.OutDateStyle
+import com.kizitonwose.calendar.data.DataStore
+import com.kizitonwose.calendar.data.getCalendarYearData
+import com.kizitonwose.calendar.data.getYearIndex
+import com.kizitonwose.calendar.data.getYearIndicesCount
+import com.kizitonwose.calendar.view.MonthDayBinder
+import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
+import com.kizitonwose.calendar.view.ViewContainer
+import com.kizitonwose.calendar.view.YearCalendarView
+import com.kizitonwose.calendar.view.YearHeaderFooterBinder
+import com.kizitonwose.calendar.view.internal.NO_INDEX
+import com.kizitonwose.calendar.view.internal.dayTag
+import com.kizitonwose.calendar.view.internal.intersects
+import com.kizitonwose.calendar.view.internal.positionYearMonth
+import java.time.DayOfWeek
+import java.time.Year
+import java.time.YearMonth
+
+internal class YearCalendarAdapter(
+ private val calView: YearCalendarView,
+ private var outDateStyle: OutDateStyle,
+ private var startYear: Year,
+ private var endYear: Year,
+ private var firstDayOfWeek: DayOfWeek,
+) : RecyclerView.Adapter() {
+ private var itemCount = getYearIndicesCount(startYear, endYear)
+ private val dataStore = DataStore { offset ->
+ getCalendarYearData(startYear, offset, firstDayOfWeek, outDateStyle)
+ }
+
+ init {
+ setHasStableIds(true)
+ }
+
+ private val isAttached: Boolean
+ get() = calView.adapter === this
+
+ override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+ calView.post { notifyYearScrollListenerIfNeeded() }
+ }
+
+ private fun getItem(position: Int): CalendarYear = dataStore[position]
+
+ override fun getItemId(position: Int): Long = getItem(position).year.value.toLong()
+
+ override fun getItemCount(): Int = itemCount
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YearViewHolder {
+ val content = setupYearItemRoot(
+ context = calView.context,
+ daySize = calView.daySize,
+ monthHeight = calView.monthHeight,
+ dayViewResource = calView.dayViewResource,
+ dayBinder = calView.dayBinder as MonthDayBinder,
+ monthColumns = calView.monthColumns,
+ monthHorizontalSpacing = calView.monthHorizontalSpacing,
+ monthVerticalSpacing = calView.monthVerticalSpacing,
+ yearItemMargins = calView.yearMargins,
+ yearBodyMargins = calView.yearBodyMargins,
+ monthHeaderResource = calView.monthHeaderResource,
+ monthFooterResource = calView.monthFooterResource,
+ monthViewClass = calView.monthViewClass,
+ monthHeaderBinder = calView.monthHeaderBinder as MonthHeaderFooterBinder?,
+ monthFooterBinder = calView.monthFooterBinder as MonthHeaderFooterBinder?,
+ yearItemViewClass = calView.yearViewClass,
+ yearItemHeaderResource = calView.yearHeaderResource,
+ yearItemFooterResource = calView.yearFooterResource,
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ return YearViewHolder(
+ rootLayout = content.itemView,
+ headerView = content.headerView,
+ footerView = content.footerView,
+ monthRowHolders = content.monthRowHolders,
+ isMonthVisible = calView.isMonthVisible,
+ yearHeaderBinder = calView.yearHeaderBinder as YearHeaderFooterBinder?,
+ yearFooterBinder = calView.yearFooterBinder as YearHeaderFooterBinder?,
+ )
+ }
+
+ override fun onBindViewHolder(holder: YearViewHolder, position: Int, payloads: List) {
+ if (payloads.isEmpty()) {
+ super.onBindViewHolder(holder, position, payloads)
+ } else {
+ payloads.forEach {
+ when (it) {
+ is CalendarDay -> holder.reloadDay(it)
+ is YearMonth -> holder.reloadMonth(it)
+ else -> {}
+ }
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: YearViewHolder, position: Int) {
+ holder.bindYear(getItem(position))
+ }
+
+ fun reloadDay(vararg day: CalendarDay) {
+ day.forEach { day ->
+ val position = getAdapterPosition(day)
+ if (position != NO_INDEX) {
+ notifyItemChanged(position, day)
+ }
+ }
+ }
+
+ fun reloadMonth(month: YearMonth) {
+ val position = getAdapterPosition(month)
+ if (position != NO_INDEX) {
+ notifyItemChanged(position, month)
+ }
+ }
+
+ fun reloadYear(year: Year) {
+ notifyItemChanged(getAdapterPosition(year))
+ }
+
+ fun reloadCalendar() {
+ notifyItemRangeChanged(0, itemCount)
+ }
+
+ private var visibleYear: CalendarYear? = null
+ fun notifyYearScrollListenerIfNeeded() {
+ // Guard for cv.post() calls and other callbacks which use this method.
+ if (!isAttached) return
+
+ if (calView.isAnimating) {
+ // Fixes an issue where findFirstVisibleMonthPosition() returns
+ // zero if called when the RecyclerView is animating. This can be
+ // replicated in Example 1 when switching from week to month mode.
+ // The property changes when switching modes in Example 1 cause
+ // notifyDataSetChanged() to be called, hence the animation.
+ calView.itemAnimator?.isRunning {
+ notifyYearScrollListenerIfNeeded()
+ }
+ return
+ }
+ val visibleItemPos = findFirstVisibleYearPosition()
+ if (visibleItemPos != RecyclerView.NO_POSITION) {
+ val visibleYear = dataStore[visibleItemPos]
+
+ if (visibleYear != this.visibleYear) {
+ this.visibleYear = visibleYear
+ calView.yearScrollListener?.invoke(visibleYear)
+
+ // See reason in MonthCalendarAdapter
+ if (calView.scrollPaged && calView.layoutParams.height == WRAP_CONTENT) {
+ val visibleVH =
+ calView.findViewHolderForAdapterPosition(visibleItemPos) ?: return
+ // Fixes #199, #266
+ visibleVH.itemView.requestLayout()
+ }
+ }
+ }
+ }
+
+ internal fun getAdapterPosition(year: Year): Int {
+ return getYearIndex(startYear, year)
+ }
+
+ internal fun getAdapterPosition(month: YearMonth): Int {
+ return getAdapterPosition(Year.of(month.year))
+ }
+
+ internal fun getAdapterPosition(day: CalendarDay): Int {
+ return getAdapterPosition(day.positionYearMonth)
+ }
+
+ private val layoutManager: YearCalendarLayoutManager
+ get() = calView.layoutManager as YearCalendarLayoutManager
+
+ fun findFirstVisibleYear(): CalendarYear? {
+ val index = findFirstVisibleYearPosition()
+ return if (index == NO_INDEX) null else dataStore[index]
+ }
+
+ fun findLastVisibleYear(): CalendarYear? {
+ val index = findLastVisibleYearPosition()
+ return if (index == NO_INDEX) null else dataStore[index]
+ }
+
+ fun findFirstVisibleMonth(): CalendarMonth? = findVisibleMonth(isFirst = true)
+
+ fun findLastVisibleMonth(): CalendarMonth? = findVisibleMonth(isFirst = false)
+
+ fun findFirstVisibleDay(): CalendarDay? = findVisibleDay(isFirst = true)
+
+ fun findLastVisibleDay(): CalendarDay? = findVisibleDay(isFirst = false)
+
+ private fun findFirstVisibleYearPosition(): Int = layoutManager.findFirstVisibleItemPosition()
+
+ private fun findLastVisibleYearPosition(): Int = layoutManager.findLastVisibleItemPosition()
+
+ /**
+ * In a vertically scrolling calendar, year and month headers/footers can cause the
+ * visible day rect to not be found in the returned visible year index from a call to
+ * findFirstVisibleItemPosition/findLastVisibleItemPosition if only the header or
+ * footer of the year or month in that index is visible. So we check adjacent indices too.
+ */
+ private fun findVisibleDay(isFirst: Boolean): CalendarDay? {
+ return visibleMonthInfo(isFirst = isFirst)?.visibleDay(isFirst)
+ ?: visibleMonthInfo(isFirst, yearIncrement = -1)?.visibleDay(isFirst)
+ ?: visibleMonthInfo(isFirst, yearIncrement = 1)?.visibleDay(isFirst)
+ }
+
+ private fun Triple.visibleDay(isFirst: Boolean): CalendarDay? {
+ val (visibleMonth, visibleMonthView, visibleMonthRect) = this
+ val dayRect = Rect()
+ return visibleMonth.weekDays.flatten()
+ .run { if (isFirst) this else reversed() }
+ .firstOrNull {
+ val dayView = visibleMonthView.findViewWithTag(dayTag(it.date))
+ ?: return@firstOrNull false
+ dayView.getGlobalVisibleRect(dayRect) &&
+ dayRect.intersects(visibleMonthRect)
+ }
+ }
+
+ /**
+ * In a vertically scrolling calendar, year headers/footers can cause the
+ * visible month rect to not be found in the returned visible year index from a call to
+ * findFirstVisibleItemPosition/findLastVisibleItemPosition if only the header or footer
+ * of the year in that index is visible. So we check adjacent indices too.
+ */
+ private fun findVisibleMonth(isFirst: Boolean): CalendarMonth? {
+ return visibleMonthInfo(isFirst = isFirst)?.first
+ ?: visibleMonthInfo(isFirst, yearIncrement = -1)?.first
+ ?: visibleMonthInfo(isFirst, yearIncrement = 1)?.first
+ }
+
+ private fun visibleMonthInfo(isFirst: Boolean, yearIncrement: Int = 0): Triple? {
+ var visibleIndex = if (isFirst) {
+ findFirstVisibleYearPosition()
+ } else {
+ findLastVisibleYearPosition()
+ }
+ if (visibleIndex == NO_INDEX) return null
+ visibleIndex += yearIncrement
+
+ val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null
+ val yearRect = Rect()
+ if (!visibleItemView.getGlobalVisibleRect(yearRect) || yearRect.isEmpty) return null
+
+ val monthRect = Rect()
+ return dataStore[visibleIndex].months
+ .run { if (isFirst) this else reversed() }
+ .firstNotNullOfOrNull {
+ val monthView = visibleItemView.findViewWithTag(monthTag(it.yearMonth))
+ ?: return@firstNotNullOfOrNull null
+ if (
+ monthView.getGlobalVisibleRect(monthRect) &&
+ monthRect.intersects(yearRect)
+ ) {
+ Triple(it, monthView, monthRect)
+ } else {
+ null
+ }
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ internal fun updateData(
+ startYear: Year,
+ endYear: Year,
+ outDateStyle: OutDateStyle,
+ firstDayOfWeek: DayOfWeek,
+ ) {
+ this.startYear = startYear
+ this.endYear = endYear
+ this.outDateStyle = outDateStyle
+ this.firstDayOfWeek = firstDayOfWeek
+ this.itemCount = getYearIndicesCount(startYear, endYear)
+ dataStore.clear()
+ notifyDataSetChanged()
+ }
+}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarLayoutManager.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarLayoutManager.kt
new file mode 100644
index 00000000..44638c5d
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarLayoutManager.kt
@@ -0,0 +1,88 @@
+package com.kizitonwose.calendar.view.internal.yearcalendar
+
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.LinearSmoothScroller
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.view.MarginValues
+import com.kizitonwose.calendar.view.YearCalendarView
+import com.kizitonwose.calendar.view.internal.CalendarLayoutManager
+import com.kizitonwose.calendar.view.internal.NO_INDEX
+import com.kizitonwose.calendar.view.internal.dayTag
+import java.time.Year
+import java.time.YearMonth
+
+internal class YearCalendarLayoutManager(private val calView: YearCalendarView) :
+ CalendarLayoutManager(calView, calView.orientation) {
+ private val adapter: YearCalendarAdapter
+ get() = calView.adapter as YearCalendarAdapter
+
+ override fun getaItemAdapterPosition(data: Year): Int = adapter.getAdapterPosition(data)
+ override fun getaDayAdapterPosition(data: CalendarDay): Int = adapter.getAdapterPosition(data)
+ override fun getDayTag(data: CalendarDay): Int = dayTag(data.date)
+ override fun getItemMargins(): MarginValues = calView.yearMargins
+ override fun scrollPaged(): Boolean = calView.scrollPaged
+ override fun notifyScrollListenerIfNeeded() = adapter.notifyYearScrollListenerIfNeeded()
+ fun smoothScrollToMonth(month: YearMonth) {
+ val indexPosition = adapter.getAdapterPosition(month)
+ if (indexPosition == NO_INDEX) return
+ // Can't target a specific month in a paged calendar.
+ startSmoothScroll(CalendarSmoothScroller(indexPosition, if (scrollPaged()) null else month))
+ }
+
+ fun scrollToMonth(month: YearMonth) {
+ val indexPosition = adapter.getAdapterPosition(month)
+ if (indexPosition == NO_INDEX) return
+ scrollToPositionWithOffset(indexPosition, 0)
+ // Can't target a specific day in a paged calendar.
+ if (scrollPaged()) {
+ calView.post { notifyScrollListenerIfNeeded() }
+ } else {
+ calView.post {
+ val itemView = calView.findViewHolderForAdapterPosition(indexPosition)?.itemView
+ ?: return@post
+ val offset = calculateDayViewOffsetInParent(month, itemView)
+ scrollToPositionWithOffset(indexPosition, -offset)
+ calView.post { notifyScrollListenerIfNeeded() }
+ }
+ }
+ }
+
+ private fun calculateDayViewOffsetInParent(month: YearMonth, itemView: View): Int {
+ val dayView = itemView.findViewWithTag(monthTag(month)) ?: return 0
+ val rect = Rect()
+ dayView.getDrawingRect(rect)
+ (itemView as ViewGroup).offsetDescendantRectToMyCoords(dayView, rect)
+ return if (orientation == VERTICAL) rect.top else rect.left
+ }
+
+ private inner class CalendarSmoothScroller(position: Int, val month: YearMonth?) :
+ LinearSmoothScroller(calView.context) {
+ init {
+ targetPosition = position
+ }
+
+ override fun getVerticalSnapPreference(): Int = SNAP_TO_START
+
+ override fun getHorizontalSnapPreference(): Int = SNAP_TO_START
+
+ override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
+ val dy = super.calculateDyToMakeVisible(view, snapPreference)
+ if (month == null) {
+ return dy
+ }
+ val offset = calculateDayViewOffsetInParent(month, view)
+ return dy - offset
+ }
+
+ override fun calculateDxToMakeVisible(view: View, snapPreference: Int): Int {
+ val dx = super.calculateDxToMakeVisible(view, snapPreference)
+ if (month == null) {
+ return dx
+ }
+ val offset = calculateDayViewOffsetInParent(month, view)
+ return dx - offset
+ }
+ }
+}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearMonthHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearMonthHolder.kt
new file mode 100644
index 00000000..c51f96dd
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearMonthHolder.kt
@@ -0,0 +1,89 @@
+package com.kizitonwose.calendar.view.internal.yearcalendar
+
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.view.DaySize
+import com.kizitonwose.calendar.view.MarginValues
+import com.kizitonwose.calendar.view.MonthDayBinder
+import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
+import com.kizitonwose.calendar.view.ViewContainer
+import com.kizitonwose.calendar.view.internal.ItemContent
+import com.kizitonwose.calendar.view.internal.setupItemRoot
+import java.time.YearMonth
+
+internal class YearMonthHolder(
+ private val daySize: DaySize,
+ private val dayViewResource: Int,
+ private var dayBinder: MonthDayBinder?,
+ private val monthHeaderResource: Int,
+ private val monthFooterResource: Int,
+ private val monthViewClass: String?,
+ private val monthHeaderBinder: MonthHeaderFooterBinder?,
+ private val monthFooterBinder: MonthHeaderFooterBinder?,
+) {
+ private lateinit var monthContainer: ItemContent
+ private var headerContainer: ViewContainer? = null
+ private var footerContainer: ViewContainer? = null
+ private lateinit var month: CalendarMonth
+
+ fun inflateMonthView(parent: LinearLayout): View {
+ return setupItemRoot(
+ itemMargins = MarginValues.ZERO,
+ daySize = daySize,
+ context = parent.context,
+ dayViewResource = dayViewResource,
+ itemHeaderResource = monthHeaderResource,
+ itemFooterResource = monthFooterResource,
+ weekSize = 6,
+ itemViewClass = monthViewClass,
+ dayBinder = dayBinder as MonthDayBinder,
+ ).also { monthContainer = it }.itemView
+ }
+
+ fun bindMonthView(month: CalendarMonth) {
+ this.month = month
+ monthContainer.itemView.apply {
+ tag = monthTag(month.yearMonth)
+ isVisible = true
+ }
+ monthContainer.headerView?.let { view ->
+ val headerContainer = headerContainer ?: monthHeaderBinder!!.create(view).also {
+ headerContainer = it
+ }
+ monthHeaderBinder?.bind(headerContainer, month)
+ }
+ monthContainer.weekHolders.forEachIndexed { index, week ->
+ week.bindWeekView(month.weekDays.getOrNull(index).orEmpty())
+ }
+ monthContainer.footerView?.let { view ->
+ val footerContainer = footerContainer ?: monthFooterBinder!!.create(view).also {
+ footerContainer = it
+ }
+ monthFooterBinder?.bind(footerContainer, month)
+ }
+ }
+
+ fun makeInvisible() {
+ monthContainer.itemView.apply {
+ tag = null
+ isInvisible = true
+ }
+ }
+
+ fun isVisible(): Boolean = monthContainer.itemView.isVisible
+
+ fun reloadMonth(yearMonth: YearMonth): Boolean {
+ return if (yearMonth == month.yearMonth) {
+ bindMonthView(month)
+ true
+ } else {
+ false
+ }
+ }
+
+ fun reloadDay(day: CalendarDay): Boolean = monthContainer.weekHolders.any { it.reloadDay(day) }
+}
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearRoot.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearRoot.kt
new file mode 100644
index 00000000..4e709429
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearRoot.kt
@@ -0,0 +1,184 @@
+package com.kizitonwose.calendar.view.internal.yearcalendar
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RectShape
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.LinearLayout
+import com.kizitonwose.calendar.view.DaySize
+import com.kizitonwose.calendar.view.MarginValues
+import com.kizitonwose.calendar.view.MonthDayBinder
+import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
+import com.kizitonwose.calendar.view.MonthHeight
+import com.kizitonwose.calendar.view.ViewContainer
+import com.kizitonwose.calendar.view.internal.customViewOrRoot
+import com.kizitonwose.calendar.view.internal.inflate
+import java.time.YearMonth
+import kotlin.math.min
+
+internal data class YearItemContent(
+ val itemView: ViewGroup,
+ val headerView: View?,
+ val footerView: View?,
+ val monthRowHolders: List>>,
+)
+
+internal fun setupYearItemRoot(
+ monthColumns: Int,
+ monthHorizontalSpacing: Int,
+ monthVerticalSpacing: Int,
+ yearItemMargins: MarginValues,
+ yearBodyMargins: MarginValues,
+ daySize: DaySize,
+ monthHeight: MonthHeight,
+ context: Context,
+ dayViewResource: Int,
+ dayBinder: MonthDayBinder?,
+ monthHeaderResource: Int,
+ monthFooterResource: Int,
+ monthViewClass: String?,
+ monthHeaderBinder: MonthHeaderFooterBinder?,
+ monthFooterBinder: MonthHeaderFooterBinder?,
+ yearItemViewClass: String?,
+ yearItemHeaderResource: Int,
+ yearItemFooterResource: Int,
+): YearItemContent {
+ val rootLayout = LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ }
+ // Put the months in a separate layout so we can have
+ // dividers that ignore the year headers and footers.
+ val monthsLayout = DividerLinearLayout(
+ context = context,
+ orientation = LinearLayout.VERTICAL,
+ axisSpacing = monthVerticalSpacing,
+ )
+
+ val itemHeaderView = if (yearItemHeaderResource != 0) {
+ rootLayout.inflate(yearItemHeaderResource).also { headerView ->
+ rootLayout.addView(headerView)
+ }
+ } else {
+ null
+ }
+ val monthCount = 12
+ val rows = (monthCount / monthColumns) + min(monthCount % monthColumns, 1)
+ val monthHolders = List(rows) {
+ val rowLayout = DividerLinearLayout(
+ context = context,
+ orientation = LinearLayout.HORIZONTAL,
+ axisSpacing = monthHorizontalSpacing,
+ )
+ val row = List(monthColumns) {
+ YearMonthHolder(
+ daySize = daySize,
+ dayViewResource = dayViewResource,
+ dayBinder = dayBinder,
+ monthHeaderResource = monthHeaderResource,
+ monthFooterResource = monthFooterResource,
+ monthViewClass = monthViewClass,
+ monthHeaderBinder = monthHeaderBinder,
+ monthFooterBinder = monthFooterBinder,
+ )
+ }.onEach { monthHolder ->
+ val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT
+ rowLayout.addView(
+ monthHolder.inflateMonthView(rowLayout),
+ LinearLayout.LayoutParams(0, height, 1f),
+ )
+ }
+ monthsLayout.addView(
+ rowLayout,
+ MonthLayoutParams(daySize, monthHeight),
+ )
+ return@List rowLayout to row
+ }
+
+ rootLayout.addView(
+ monthsLayout,
+ MonthLayoutParams(daySize, monthHeight).apply {
+ bottomMargin = yearBodyMargins.bottom
+ topMargin = yearBodyMargins.top
+ marginStart = yearBodyMargins.start
+ marginEnd = yearBodyMargins.end
+ },
+ )
+
+ val itemFooterView = if (yearItemFooterResource != 0) {
+ rootLayout.inflate(yearItemFooterResource).also { footerView ->
+ rootLayout.addView(footerView)
+ }
+ } else {
+ null
+ }
+
+ val itemView = customViewOrRoot(
+ customViewClass = yearItemViewClass,
+ rootLayout = rootLayout,
+ ) { root: ViewGroup ->
+ val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT
+ val height = when (monthHeight) {
+ MonthHeight.FollowDaySize -> if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT
+ MonthHeight.Fill -> MATCH_PARENT
+ }
+ root.layoutParams = MarginLayoutParams(width, height).apply {
+ bottomMargin = yearItemMargins.bottom
+ topMargin = yearItemMargins.top
+ marginStart = yearItemMargins.start
+ marginEnd = yearItemMargins.end
+ }
+ }
+
+ return YearItemContent(
+ itemView = itemView,
+ headerView = itemHeaderView,
+ footerView = itemFooterView,
+ monthRowHolders = monthHolders,
+ )
+}
+
+@Suppress("FunctionName")
+private fun DividerLinearLayout(
+ context: Context,
+ orientation: Int,
+ axisSpacing: Int,
+) = LinearLayout(context).apply {
+ this.orientation = orientation
+ if (axisSpacing > 0) {
+ showDividers = LinearLayout.SHOW_DIVIDER_MIDDLE
+ dividerDrawable = ShapeDrawable(RectShape()).apply {
+ if (orientation == LinearLayout.VERTICAL) {
+ intrinsicHeight = axisSpacing
+ } else {
+ intrinsicWidth = axisSpacing
+ }
+ paint.color = Color.TRANSPARENT
+ }
+ }
+}
+
+@Suppress("FunctionName")
+private fun MonthLayoutParams(
+ daySize: DaySize,
+ monthHeight: MonthHeight,
+): LinearLayout.LayoutParams {
+ val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT
+ return when (monthHeight) {
+ MonthHeight.FollowDaySize -> {
+ val height = if (daySize.parentDecidesHeight) 0 else WRAP_CONTENT
+ val weight = if (daySize.parentDecidesHeight) 1f else 0f
+ LinearLayout.LayoutParams(width, height, weight)
+ }
+
+ MonthHeight.Fill -> {
+ LinearLayout.LayoutParams(width, 0, 1f)
+ }
+ }
+}
+
+internal fun monthTag(month: YearMonth): Int = month.hashCode()
diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearViewHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearViewHolder.kt
new file mode 100644
index 00000000..55731ae7
--- /dev/null
+++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearViewHolder.kt
@@ -0,0 +1,74 @@
+package com.kizitonwose.calendar.view.internal.yearcalendar
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import com.kizitonwose.calendar.core.CalendarDay
+import com.kizitonwose.calendar.core.CalendarMonth
+import com.kizitonwose.calendar.core.CalendarYear
+import com.kizitonwose.calendar.view.ViewContainer
+import com.kizitonwose.calendar.view.YearHeaderFooterBinder
+import java.time.YearMonth
+
+internal class YearViewHolder(
+ rootLayout: ViewGroup,
+ private val headerView: View?,
+ private val footerView: View?,
+ private val monthRowHolders: List>>,
+ private val yearHeaderBinder: YearHeaderFooterBinder?,
+ private val yearFooterBinder: YearHeaderFooterBinder?,
+ private val isMonthVisible: (month: CalendarMonth) -> Boolean,
+) : RecyclerView.ViewHolder(rootLayout) {
+ private var yearHeaderContainer: ViewContainer? = null
+ private var yearFooterContainer: ViewContainer? = null
+
+ lateinit var year: CalendarYear
+
+ fun bindYear(year: CalendarYear) {
+ this.year = year
+ headerView?.let { view ->
+ val headerContainer = yearHeaderContainer ?: yearHeaderBinder!!.create(view).also {
+ yearHeaderContainer = it
+ }
+ yearHeaderBinder?.bind(headerContainer, year)
+ }
+ val months = year.months.filter(isMonthVisible)
+ var index = 0
+ for ((rowLayout, row) in monthRowHolders) {
+ for (monthHolder in row) {
+ if (months.size > index) {
+ monthHolder.bindMonthView(months[index])
+ } else {
+ monthHolder.makeInvisible()
+ }
+ index += 1
+ }
+ rowLayout.isVisible = row.any(YearMonthHolder::isVisible)
+ }
+ footerView?.let { view ->
+ val footerContainer = yearFooterContainer ?: yearFooterBinder!!.create(view).also {
+ yearFooterContainer = it
+ }
+ yearFooterBinder?.bind(footerContainer, year)
+ }
+ }
+
+ fun reloadMonth(yearMonth: YearMonth) {
+ visibleItems().firstOrNull {
+ it.reloadMonth(yearMonth)
+ }
+ }
+
+ fun reloadDay(day: CalendarDay) {
+ visibleItems().firstOrNull {
+ it.reloadDay(day)
+ }
+ }
+
+ private fun visibleItems() = monthRowHolders
+ .map { it.second }
+ .flatten()
+ .filter { it.isVisible() }
+}
diff --git a/view/src/main/res/values/attrs.xml b/view/src/main/res/values/attrs.xml
index 4b3787a9..b9f7094b 100644
--- a/view/src/main/res/values/attrs.xml
+++ b/view/src/main/res/values/attrs.xml
@@ -3,6 +3,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -30,35 +58,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -80,4 +84,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+