From 7a11662fd868b8e7d04f988d6eea6813409d46d2 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 27 Jul 2024 19:05:25 +0200 Subject: [PATCH 01/18] Setup YearCalendarView class and layout manager --- .../com/kizitonwose/calendar/view/Binder.kt | 13 +- .../calendar/view/YearCalendarView.kt | 554 ++++++++++++++++++ .../calendar/view/internal/DayHolder.kt | 1 + .../calendar/view/internal/MonthHolder.kt | 114 ++++ .../calendar/view/internal/Utils.kt | 2 +- .../internal/monthcalendar/MonthViewHolder.kt | 7 +- .../internal/weekcalendar/WeekViewHolder.kt | 4 +- .../yearcalendar/YearCalendarAdapter.kt | 249 ++++++++ .../yearcalendar/YearCalendarLayoutManager.kt | 21 + .../view/internal/yearcalendar/YearRoot.kt | 143 +++++ .../internal/yearcalendar/YearViewHolder.kt | 61 ++ view/src/main/res/values/attrs.xml | 35 ++ 12 files changed, 1193 insertions(+), 11 deletions(-) create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarLayoutManager.kt create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearRoot.kt create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearViewHolder.kt 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/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt new file mode 100644 index 00000000..09812d7f --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -0,0 +1,554 @@ +package com.kizitonwose.calendar.view + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +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.DayPosition +import com.kizitonwose.calendar.core.OutDateStyle +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.monthcalendar.MonthCalendarAdapter +import com.kizitonwose.calendar.view.internal.monthcalendar.MonthCalendarLayoutManager +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 + +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 header views. + * The header view is shown above each month on the Calendar. + */ + public var monthHeaderBinder: MonthHeaderFooterBinder<*>? = null + set(value) { + field = value + invalidateViewHolders() + } + + /** + * The [MonthHeaderFooterBinder] instance used for managing footer views. + * The footer view is shown below each month on the Calendar. + */ + public var monthFooterBinder: MonthHeaderFooterBinder<*>? = null + set(value) { + field = value + invalidateViewHolders() + } + + /** + * The [YearHeaderFooterBinder] instance used for managing header views. + * The header view is shown above each year on the Calendar. + */ + public var yearHeaderBinder: YearHeaderFooterBinder<*>? = null + set(value) { + field = value + invalidateViewHolders() + } + + /** + * The [YearHeaderFooterBinder] instance used for managing footer views. + * The footer view is 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 month. + * 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) { + check(value != 0) { "Invalid 'dayViewResource' value." } + field = value + invalidateViewHolders() + } + } + + /** + * The xml resource that is inflated and used as a header for every 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 every 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 every 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 every year. + * Set zero to disable. + */ + public var yearFooterResource: Int = 0 + set(value) { + if (field != value) { + field = value + invalidateViewHolders() + } + } + + /** + * A [ViewGroup] which 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 monthViewClass: String? = null + set(value) { + if (field != value) { + field = value + invalidateViewHolders() + } + } + + /** + * A [ViewGroup] which 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 [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? MonthCalendarLayoutManager)?.orientation = value + updateSnapHelper() + } + } + + /** + * The scrolling behavior of the calendar. If `true`, the calendar will + * snap to the nearest month 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. + * Can be [DaySize.Square], [DaySize.SeventhWidth] or [DaySize.FreeForm]. + */ + public var daySize: DaySize = DaySize.Square + set(value) { + if (field != value) { + field = value + invalidateViewHolders() + } + } + + /** + * The margins, in pixels to be applied on each month view. + * this can be used to add a space between two months. + */ + public var yearMargins: MarginValues = MarginValues() + 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.notifyMonthScrollListenerIfNeeded() + } + } + } + + 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, + ) + 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), + ] + outDateStyle = OutDateStyle.entries[ + getInt(R.styleable.YearCalendarView_cv_outDateStyle, outDateStyle.ordinal), + ] + monthViewClass = getString(R.styleable.YearCalendarView_cv_monthViewClass) + } + 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 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.scrollToIndex(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.smoothScrollToIndex(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 CalendarView 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)) + } + + // This could replace the other `notifyDateChanged` with one DayPosition param if we add + // the `JvmOverloads` annotation but that would break compatibility in places where the + // method is called with named args: notifyDateChanged(date = *, position = DayPosition.*) + // because assigning single elements to varargs in named form is not allowed. + // May consider removing the other one at some point. + + /** + * Notify the CalendarView 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 CalendarView to reload the view for this [YearMonth] + * 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. + */ + public fun notifyMonthChanged(month: YearMonth) { + calendarAdapter.reloadMonth(month) + } + + /** + * Notify the CalendarView to reload all months. + * @see [notifyMonthChanged]. + */ + public fun notifyCalendarChanged() { + calendarAdapter.reloadCalendar() + } + + /** + * Find the first visible month on the CalendarView. + * + * @return The first visible month or null if not found. + */ + public fun findFirstVisibleMonth(): CalendarMonth? { + return calendarAdapter.findFirstVisibleMonth() + } + + /** + * Find the last visible month on the CalendarView. + * + * @return The last visible month or null if not found. + */ + public fun findLastVisibleMonth(): CalendarMonth? { + return calendarAdapter.findLastVisibleMonth() + } + + /** + * Find the first visible day on the CalendarView. + * 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 CalendarView. + * 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 CalendarView. + * 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. + */ + 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 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. + * 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 updateMonthData( + 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 = startYear ?: throw getFieldException("startYear") + + private fun requireEndYear(): Year = endYear ?: throw getFieldException("endYear") + + private fun requireFirstDayOfWeek(): DayOfWeek = + firstDayOfWeek ?: throw getFieldException("firstDayOfWeek") + + private fun getFieldException(field: String) = + IllegalStateException("`$field` is not set. Have you called `setup()`?") +} 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..e3c0f4df 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 @@ -81,6 +81,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/MonthHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt new file mode 100644 index 00000000..0daadc6a --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt @@ -0,0 +1,114 @@ +package com.kizitonwose.calendar.view.internal + +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.isGone +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 java.time.YearMonth + +internal class MonthHolder( + 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 var monthHeaderBinder: MonthHeaderFooterBinder?, + private var 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( + // TODO - YEAR + itemMargins = MarginValues(), + daySize = daySize, + context = parent.context, + dayViewResource = dayViewResource, + itemHeaderResource = monthHeaderResource, + itemFooterResource = monthFooterResource, + weekSize = 6, + // TODO - YEAR + itemViewClass = null, + dayBinder = dayBinder as MonthDayBinder, + ).also { monthContainer = it }.itemView + +// return LinearLayout(parent.context).apply { +// monthContainer = this +// // TODO - YEAR +//// val width = if (daySize.parentDecidesWidth) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT +//// val height = if (daySize.parentDecidesHeight) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT +//// val weight = if (daySize.parentDecidesHeight) 1f else 0f +// layoutParams = GridLayout.LayoutParams( +// GridLayout.spec( +// /* start = */ GridLayout.UNDEFINED, +// /* size = */ 1, +// /* alignment = */ GridLayout.FILL, +// ), +// GridLayout.spec( +// /* start = */ GridLayout.UNDEFINED, +// /* size = */ 1, +// /* alignment = */ +// if (daySize.parentDecidesHeight) { +// GridLayout.FILL +// } else { +// GridLayout.TOP +// }, +// ), +// ) +// orientation = LinearLayout.VERTICAL +// for (holder in weekHolders) { +// addView(holder.inflateWeekView(this)) +// } +// } + } + + fun bindMonthView(month: CalendarMonth) { + monthContainer.itemView.isGone = false + // The last week row can be empty if out date style is not `EndOfGrid` + this.month = month + 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 hide() { + monthContainer.itemView.isGone = true + } + + fun isShown(): Boolean = monthContainer.itemView.isVisible + + fun reloadMonth(yearMonth: YearMonth): Boolean { + return if (yearMonth == this.month.yearMonth) { + bindMonthView(this.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/Utils.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt index f52981a6..f2187886 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt @@ -110,5 +110,5 @@ internal fun setupItemRoot( } internal fun dayTag(date: LocalDate): Int = date.hashCode() -private const val EXAMPLE_CUSTOM_CLASS_URL = +internal 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/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/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..a556744c --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt @@ -0,0 +1,249 @@ +package com.kizitonwose.calendar.view.internal.yearcalendar + +import android.annotation.SuppressLint +import android.graphics.Rect +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.CalendarYear +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.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 java.time.DayOfWeek +import java.time.Month +import java.time.Year + +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( + daySize = calView.daySize, + context = calView.context, + dayViewResource = calView.dayViewResource, + dayBinder = calView.dayBinder as MonthDayBinder, + columns = 3, // todo move to property + itemCount = itemCount, + yearItemMargins = calView.yearMargins, + 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, + monthHolders = content.monthHolders, + 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 { + holder.reloadDay(it as CalendarDay) + } + } + } + + 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: Year) { + notifyItemChanged(getAdapterPosition(month)) + } + + 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) + + // TODO - YEAR + // Fixes issue where the calendar does not resize its height when in horizontal, paged mode and + // the `outDateStyle` is not `endOfGrid` hence the last row of a 5-row visible month is empty. + // We set such week row's container visibility to GONE in the WeekHolder but it seems the + // RecyclerView accounts for the items in the immediate previous and next indices when + // calculating height and uses the tallest one of the three meaning that the current index's + // view will end up having a blank space at the bottom unless the immediate previous and next + // indices are also missing the last row. I think there should be a better way to fix this. + // New: Also fixes issue where the calendar does not wrap each month's height when in vertical, + // paged mode and just matches parent's height instead. + // Only happens when the CalendarView wraps its height. + 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(day: CalendarDay): Int { + return getAdapterPosition(day.positionYear) + } + + 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 findFirstVisibleDay(): CalendarDay? = findVisibleDay(true) + + fun findLastVisibleDay(): CalendarDay? = findVisibleDay(false) + + private fun findFirstVisibleYearPosition(): Int = layoutManager.findFirstVisibleItemPosition() + + private fun findLastVisibleYearPosition(): Int = layoutManager.findLastVisibleItemPosition() + + private fun findVisibleDay(isFirst: Boolean): CalendarDay? { + val visibleIndex = + if (isFirst) findFirstVisibleYearPosition() else findLastVisibleYearPosition() + if (visibleIndex == NO_INDEX) return null + + val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null + val monthRect = Rect() + visibleItemView.getGlobalVisibleRect(monthRect) + // TODO - YEAR + +// val dayRect = Rect() +// return dataStore[visibleIndex].weekDays.flatten() +// .run { if (isFirst) this else reversed() } +// .firstOrNull { +// val dayView = visibleItemView.findViewWithTag(dayTag(it.date)) +// ?: return@firstOrNull false +// dayView.getGlobalVisibleRect(dayRect) +// dayRect.intersect(monthRect) +// } + return 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() + } +} + +// Find the actual year on the calendar where this date is shown. +internal val CalendarDay.positionYear: Year + get() = when (position) { + DayPosition.InDate -> if (date.month == Month.DECEMBER) { + date.yearMonth.nextMonth.year + } else { + date.yearMonth.year + } + + DayPosition.MonthDate -> date.year + DayPosition.OutDate -> if (date.month == Month.JANUARY) { + date.yearMonth.previousMonth.year + } else { + date.yearMonth.year + } + }.let(Year::of) + 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..578eabd6 --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarLayoutManager.kt @@ -0,0 +1,21 @@ +package com.kizitonwose.calendar.view.internal.yearcalendar + +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.dayTag +import java.time.Year + +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() +} 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..24655860 --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearRoot.kt @@ -0,0 +1,143 @@ +package com.kizitonwose.calendar.view.internal.yearcalendar + +import android.content.Context +import android.util.Log +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.ViewContainer +import com.kizitonwose.calendar.view.internal.EXAMPLE_CUSTOM_CLASS_URL +import com.kizitonwose.calendar.view.internal.MonthHolder +import com.kizitonwose.calendar.view.internal.inflate +import java.time.LocalDate +import kotlin.math.min + +internal data class YearItemContent( + val itemView: ViewGroup, + val headerView: View?, + val footerView: View?, + val monthHolders: List>, +) + +internal fun setupYearItemRoot( + columns: Int, + itemCount: Int, + yearItemMargins: MarginValues, + daySize: DaySize, + 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 + } + + val itemHeaderView = if (yearItemHeaderResource != 0) { + rootLayout.inflate(yearItemHeaderResource).also { headerView -> + rootLayout.addView(headerView) + } + } else { + null + } + val rows = (itemCount / columns) + min(1, itemCount.mod(columns)) + val monthHolders = List(rows) { + val rowLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + } + // TODO - YEAR optimize size ignore unused index after filter + val row = List(columns) { + MonthHolder( + daySize = daySize, + dayViewResource = dayViewResource, + dayBinder = dayBinder, + monthHeaderResource = monthHeaderResource, + monthFooterResource = monthFooterResource, + monthViewClass = monthViewClass, + monthHeaderBinder = monthHeaderBinder, + monthFooterBinder = monthFooterBinder, + ) + }.onEach { monthHolder -> + // todo weight + val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT + val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT + rowLayout.addView( + monthHolder.inflateMonthView(rowLayout), + LinearLayout.LayoutParams(width, height, 1f), + ) + } + // todo weight + val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT + val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT + val weight = if (daySize.parentDecidesHeight) 1f else 0f + rootLayout.addView( + rowLayout, + LinearLayout.LayoutParams(width, height, weight), + ) + return@List row + } + + val itemFooterView = if (yearItemFooterResource != 0) { + rootLayout.inflate(yearItemFooterResource).also { footerView -> + rootLayout.addView(footerView) + } + } else { + null + } + + fun setupRoot(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 { + bottomMargin = yearItemMargins.bottom + topMargin = yearItemMargins.top + marginStart = yearItemMargins.start + marginEnd = yearItemMargins.end + } + } + + val itemView = yearItemViewClass?.let { + val customLayout = runCatching { + Class.forName(it) + .getDeclaredConstructor(Context::class.java) + .newInstance(rootLayout.context) as ViewGroup + }.onFailure { + Log.e( + "CalendarView", + "Failure loading custom class $yearItemViewClass, " + + "check that $yearItemViewClass 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 YearItemContent( + itemView = itemView, + headerView = itemHeaderView, + footerView = itemFooterView, + monthHolders = monthHolders, + ) +} + +internal fun dayTag(date: LocalDate): Int = date.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..9a876621 --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearViewHolder.kt @@ -0,0 +1,61 @@ +package com.kizitonwose.calendar.view.internal.yearcalendar + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.view.ViewContainer +import com.kizitonwose.calendar.view.YearHeaderFooterBinder +import com.kizitonwose.calendar.view.internal.MonthHolder +import java.time.YearMonth + +internal class YearViewHolder( + rootLayout: ViewGroup, + private val headerView: View?, + private val footerView: View?, + private val monthHolders: List>, + private val yearHeaderBinder: YearHeaderFooterBinder?, + private val yearFooterBinder: YearHeaderFooterBinder?, +) : 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 { true } + monthHolders.flatten().forEachIndexed { index, month -> + if (months.size > index) { + month.bindMonthView(months[index]) + } else { + month.hide() + } + } + footerView?.let { view -> + val footerContainer = yearFooterContainer ?: yearFooterBinder!!.create(view).also { + yearFooterContainer = it + } + yearFooterBinder?.bind(footerContainer, year) + } + } + + fun reloadMonth(yearMonth: YearMonth) { + monthHolders.flatten() + .filter { it.isShown() } + .firstOrNull { it.reloadMonth(yearMonth) } + } + + fun reloadDay(day: CalendarDay) { + monthHolders.flatten() + .filter { it.isShown() } + .firstOrNull { it.reloadDay(day) } + } +} diff --git a/view/src/main/res/values/attrs.xml b/view/src/main/res/values/attrs.xml index 4b3787a9..859521df 100644 --- a/view/src/main/res/values/attrs.xml +++ b/view/src/main/res/values/attrs.xml @@ -80,4 +80,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fbfdb7a0cd926489f3cdfaf63113cafb6759f74d Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 28 Jul 2024 09:27:13 +0200 Subject: [PATCH 02/18] Add basic year view sample --- .../sample/view/CalendarViewOptionsAdapter.kt | 5 + .../calendar/sample/view/Example9Fragment.kt | 141 ++++++++++++++++++ .../res/layout/example_9_calendar_day.xml | 11 ++ .../example_9_calendar_month_header.xml | 26 ++++ .../layout/example_9_calendar_year_header.xml | 36 +++++ .../main/res/layout/example_9_fragment.xml | 37 +++++ sample/src/main/res/values/strings.xml | 2 + .../calendar/view/YearCalendarView.kt | 64 +++++++- .../yearcalendar/YearCalendarAdapter.kt | 16 +- .../yearcalendar/YearCalendarLayoutManager.kt | 8 + .../view/internal/yearcalendar/YearRoot.kt | 10 +- view/src/main/res/values/attrs.xml | 94 ++++++------ 12 files changed, 389 insertions(+), 61 deletions(-) create mode 100644 sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt create mode 100644 sample/src/main/res/layout/example_9_calendar_day.xml create mode 100644 sample/src/main/res/layout/example_9_calendar_month_header.xml create mode 100644 sample/src/main/res/layout/example_9_calendar_year_header.xml create mode 100644 sample/src/main/res/layout/example_9_fragment.xml 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..eede1885 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,11 @@ 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() }, ) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionsViewHolder { 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..1366fcbb --- /dev/null +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt @@ -0,0 +1,141 @@ +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 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.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 + +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 + private val today = LocalDate.now() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + binding = Example9FragmentBinding.bind(view) + configureBinders() + binding.exNineCalendar.setup( + Year.now(), + Year.now().plusYears(50), + firstDayOfWeekFromLocale(), + ) + } + + private fun configureBinders() { + 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 + + 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) + } + + today -> { + textView.setTextColorRes(R.color.example_2_red) + textView.background = null + } + + else -> { + textView.setTextColorRes(R.color.example_2_black) + textView.background = null + } + } + } else { + textView.makeInVisible() + } + } + } + + class MonthViewContainer(view: View) : ViewContainer(view) { + val bind = Example9CalendarMonthHeaderBinding.bind(view) + val textView = bind.exNineMonthHeaderText + val legendLayout = bind.legendLayout.root + } + calendarView.monthHeaderBinder = + object : MonthHeaderFooterBinder { + override fun create(view: View) = MonthViewContainer(view) + override fun bind(container: MonthViewContainer, data: CalendarMonth) { + container.textView.text = data.yearMonth.displayText() + // 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) + // TODO - YEAR set menium bold +// tv.sty + } + } + } + } + + class YearViewContainer(view: View) : ViewContainer(view) { + val textView = Example9CalendarYearHeaderBinding.bind(view).exNineYearHeaderText + } + 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/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..7b1998c2 --- /dev/null +++ b/sample/src/main/res/layout/example_9_calendar_year_header.xml @@ -0,0 +1,36 @@ + + + + + + + + + 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/values/strings.xml b/sample/src/main/res/values/strings.xml index 68fe6b87..227ff710 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -17,6 +17,8 @@ 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. Save Close Enter event title diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index 09812d7f..22fe47e1 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -8,12 +8,12 @@ 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.data.checkRange import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelper import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelperLegacy -import com.kizitonwose.calendar.view.internal.monthcalendar.MonthCalendarAdapter import com.kizitonwose.calendar.view.internal.monthcalendar.MonthCalendarLayoutManager import com.kizitonwose.calendar.view.internal.yearcalendar.YearCalendarAdapter import com.kizitonwose.calendar.view.internal.yearcalendar.YearCalendarLayoutManager @@ -239,7 +239,7 @@ public open class YearCalendarView : RecyclerView { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {} override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { if (newState == SCROLL_STATE_IDLE) { - calendarAdapter.notifyMonthScrollListenerIfNeeded() + calendarAdapter.notifyYearScrollListenerIfNeeded() } } } @@ -285,6 +285,14 @@ public open class YearCalendarView : RecyclerView { 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( @@ -298,6 +306,8 @@ public open class YearCalendarView : RecyclerView { getInt(R.styleable.YearCalendarView_cv_outDateStyle, outDateStyle.ordinal), ] monthViewClass = getString(R.styleable.YearCalendarView_cv_monthViewClass) + // TODO - YEAR class + // monthViewClass = getString(R.styleable.YearCalendarView_cv_monthViewClass) } check(dayViewResource != 0) { "No value set for `cv_dayViewResource` attribute." } } @@ -339,13 +349,30 @@ public open class YearCalendarView : RecyclerView { 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 [smoothScrollToMonth] + */ + public fun scrollToYear(year: Year) { + calendarLayoutManager.scrollToIndex(year) + } + + /** + * Scroll to a specific year on the calendar using a smooth scrolling animation. + * Just like [scrollToMonth], 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.scrollToIndex(month) + calendarLayoutManager.scrollToMonth(month) } /** @@ -353,7 +380,7 @@ public open class YearCalendarView : RecyclerView { * Just like [scrollToMonth], but with a smooth scrolling animation. */ public fun smoothScrollToMonth(month: YearMonth) { - calendarLayoutManager.smoothScrollToIndex(month) + calendarLayoutManager.smoothScrollToMonth(month) } /** @@ -441,6 +468,17 @@ public open class YearCalendarView : RecyclerView { calendarAdapter.reloadMonth(month) } + /** + * Notify the CalendarView to reload the view for this [YearMonth] + * 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. + */ + public fun notifyYearChanged(year: Year) { + calendarAdapter.reloadYear(year) + } + /** * Notify the CalendarView to reload all months. * @see [notifyMonthChanged]. @@ -467,6 +505,24 @@ public open class YearCalendarView : RecyclerView { return calendarAdapter.findLastVisibleMonth() } + /** + * Find the first visible year on the CalendarView. + * + * @return The first visible year or null if not found. + */ + public fun findFirstVisibleYear(): CalendarYear? { + return calendarAdapter.findFirstVisibleYear() + } + + /** + * Find the last visible year on the CalendarView. + * + * @return The last visible year or null if not found. + */ + public fun findLastVisibleYear(): CalendarYear? { + return calendarAdapter.findLastVisibleYear() + } + /** * Find the first visible day on the CalendarView. * This is the day at the top-left corner of the calendar. 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 index a556744c..87816bf5 100644 --- 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 @@ -6,6 +6,7 @@ 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.DayPosition import com.kizitonwose.calendar.core.OutDateStyle @@ -25,6 +26,7 @@ import com.kizitonwose.calendar.view.internal.NO_INDEX import java.time.DayOfWeek import java.time.Month import java.time.Year +import java.time.YearMonth internal class YearCalendarAdapter( private val calView: YearCalendarView, @@ -109,7 +111,19 @@ internal class YearCalendarAdapter( } } - fun reloadMonth(month: Year) { + fun reloadMonth(month: YearMonth) { + // TODO - YEAR + } + + fun findFirstVisibleMonth(): CalendarMonth? { + TODO("Not yet implemented") + } + + fun findLastVisibleMonth(): CalendarMonth? { + TODO("Not yet implemented") + } + + fun reloadYear(month: Year) { notifyItemChanged(getAdapterPosition(month)) } 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 index 578eabd6..154c03af 100644 --- 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 @@ -6,6 +6,7 @@ import com.kizitonwose.calendar.view.YearCalendarView import com.kizitonwose.calendar.view.internal.CalendarLayoutManager 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) { @@ -18,4 +19,11 @@ internal class YearCalendarLayoutManager(private val calView: YearCalendarView) override fun getItemMargins(): MarginValues = calView.yearMargins override fun scrollPaged(): Boolean = calView.scrollPaged override fun notifyScrollListenerIfNeeded() = adapter.notifyYearScrollListenerIfNeeded() + fun smoothScrollToMonth(month: YearMonth) { + // TODO - YEAR + } + + fun scrollToMonth(month: YearMonth) { + // TODO - YEAR + } } 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 index 24655860..25b0b654 100644 --- 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 @@ -54,10 +54,10 @@ internal fun setupYearItemRoot( } else { null } - val rows = (itemCount / columns) + min(1, itemCount.mod(columns)) + val rows = (itemCount / columns) + min(1, itemCount.rem(columns)) val monthHolders = List(rows) { val rowLayout = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL + orientation = LinearLayout.HORIZONTAL } // TODO - YEAR optimize size ignore unused index after filter val row = List(columns) { @@ -77,12 +77,12 @@ internal fun setupYearItemRoot( val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT rowLayout.addView( monthHolder.inflateMonthView(rowLayout), - LinearLayout.LayoutParams(width, height, 1f), + LinearLayout.LayoutParams(0, height, 1f), ) } // todo weight val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT - val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT + val height = if (daySize.parentDecidesHeight) 0 else WRAP_CONTENT val weight = if (daySize.parentDecidesHeight) 1f else 0f rootLayout.addView( rowLayout, @@ -117,7 +117,7 @@ internal fun setupYearItemRoot( .newInstance(rootLayout.context) as ViewGroup }.onFailure { Log.e( - "CalendarView", + "YearCalendarView", "Failure loading custom class $yearItemViewClass, " + "check that $yearItemViewClass is a ViewGroup and the " + "single argument context constructor is available. " + diff --git a/view/src/main/res/values/attrs.xml b/view/src/main/res/values/attrs.xml index 859521df..d74b56bf 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -83,35 +87,23 @@ + + - - + + - - - - - - - - - - + + + - - - - - - - + + + From 5cc1bff14718dd3e4c0d67d264ebda55c0a7abb0 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 28 Jul 2024 10:07:28 +0200 Subject: [PATCH 03/18] Improve sample on small screens --- .../calendar/sample/view/Example9Fragment.kt | 57 +++++++---- .../calendar/sample/view/Typeface.kt | 16 ++++ .../layout/example_9_calendar_year_header.xml | 3 +- .../kizitonwose/calendar/view/MarginValues.kt | 12 ++- .../calendar/view/YearCalendarView.kt | 43 +++++++++ .../calendar/view/internal/CustomViewClass.kt | 36 +++++++ .../calendar/view/internal/Utils.kt | 30 +----- .../yearcalendar/YearCalendarAdapter.kt | 5 +- .../view/internal/yearcalendar/YearRoot.kt | 95 +++++++++++-------- view/src/main/res/values/attrs.xml | 10 ++ 10 files changed, 219 insertions(+), 88 deletions(-) create mode 100644 sample/src/main/java/com/kizitonwose/calendar/sample/view/Typeface.kt create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/internal/CustomViewClass.kt 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 index 1366fcbb..9b4bfa21 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt @@ -5,6 +5,8 @@ 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 @@ -16,6 +18,7 @@ import com.kizitonwose.calendar.sample.databinding.Example9CalendarMonthHeaderBi 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 @@ -38,21 +41,34 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) binding = Example9FragmentBinding.bind(view) - configureBinders() - binding.exNineCalendar.setup( - Year.now(), - Year.now().plusYears(50), - firstDayOfWeekFromLocale(), - ) + val config = requireContext().resources.configuration + val isTablet = config.smallestScreenWidthDp >= 600 + + configureBinders(isTablet) + + binding.exNineCalendar.apply { + monthVerticalSpacing = dpToPx(20, requireContext()) + monthHorizontalSpacing = dpToPx(if (isTablet) 52 else 10, requireContext()) + yearMargins = MarginValues( + horizontal = dpToPx(if (isTablet) 52 else 14, requireContext()), + ) + setup( + Year.now(), + Year.now().plusYears(50), + firstDayOfWeekFromLocale(), + ) + } } - private fun configureBinders() { + 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 + val textView = Example9CalendarDayBinding.bind(view).exNineDayText.apply { + textSize = if (isTablet) 10f else 9f + } init { textView.setOnClickListener { @@ -86,11 +102,6 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, 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 @@ -102,33 +113,43 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, } } + val monthNameTypeFace = Typeface.semiBold(requireContext()) + class MonthViewContainer(view: View) : ViewContainer(view) { val bind = Example9CalendarMonthHeaderBinding.bind(view) - val textView = bind.exNineMonthHeaderText + 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 } calendarView.monthHeaderBinder = object : MonthHeaderFooterBinder { override fun create(view: View) = MonthViewContainer(view) override fun bind(container: MonthViewContainer, data: CalendarMonth) { - container.textView.text = data.yearMonth.displayText() + 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 } + val typeface = Typeface.medium(requireContext()) 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) - // TODO - YEAR set menium bold -// tv.sty + tv.textSize = if (isTablet) 14f else 11f + tv.setTypeface(typeface) } } } } class YearViewContainer(view: View) : ViewContainer(view) { - val textView = Example9CalendarYearHeaderBinding.bind(view).exNineYearHeaderText + 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 { 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_9_calendar_year_header.xml b/sample/src/main/res/layout/example_9_calendar_year_header.xml index 7b1998c2..25d6caab 100644 --- a/sample/src/main/res/layout/example_9_calendar_year_header.xml +++ b/sample/src/main/res/layout/example_9_calendar_year_header.xml @@ -25,8 +25,7 @@ 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/Utils.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt index f2187886..4dc13419 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/Utils.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() -internal 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/yearcalendar/YearCalendarAdapter.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt index 87816bf5..3f6b8227 100644 --- 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 @@ -64,8 +64,9 @@ internal class YearCalendarAdapter( context = calView.context, dayViewResource = calView.dayViewResource, dayBinder = calView.dayBinder as MonthDayBinder, - columns = 3, // todo move to property - itemCount = itemCount, + monthColumns = calView.monthColumns, + monthHorizontalSpacing = calView.monthHorizontalSpacing, + monthVerticalSpacing = calView.monthVerticalSpacing, yearItemMargins = calView.yearMargins, monthHeaderResource = calView.monthHeaderResource, monthFooterResource = calView.monthFooterResource, 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 index 25b0b654..524feae0 100644 --- 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 @@ -1,7 +1,9 @@ package com.kizitonwose.calendar.view.internal.yearcalendar import android.content.Context -import android.util.Log +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 @@ -13,10 +15,9 @@ 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.EXAMPLE_CUSTOM_CLASS_URL import com.kizitonwose.calendar.view.internal.MonthHolder +import com.kizitonwose.calendar.view.internal.customViewOrRoot import com.kizitonwose.calendar.view.internal.inflate -import java.time.LocalDate import kotlin.math.min internal data class YearItemContent( @@ -27,8 +28,9 @@ internal data class YearItemContent( ) internal fun setupYearItemRoot( - columns: Int, - itemCount: Int, + monthColumns: Int, + monthHorizontalSpacing: Int, + monthVerticalSpacing: Int, yearItemMargins: MarginValues, daySize: DaySize, context: Context, @@ -46,6 +48,13 @@ internal fun setupYearItemRoot( 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 -> @@ -54,13 +63,15 @@ internal fun setupYearItemRoot( } else { null } - val rows = (itemCount / columns) + min(1, itemCount.rem(columns)) + val monthCount = 12 + val rows = (monthCount / monthColumns) + min(1, monthCount % monthColumns) val monthHolders = List(rows) { - val rowLayout = LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - } - // TODO - YEAR optimize size ignore unused index after filter - val row = List(columns) { + val rowLayout = DividerLinearLayout( + context = context, + orientation = LinearLayout.HORIZONTAL, + axisSpacing = monthHorizontalSpacing, + ) + val row = List(monthColumns) { MonthHolder( daySize = daySize, dayViewResource = dayViewResource, @@ -72,25 +83,32 @@ internal fun setupYearItemRoot( monthFooterBinder = monthFooterBinder, ) }.onEach { monthHolder -> - // todo weight - val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT val height = if (daySize.parentDecidesHeight) MATCH_PARENT else WRAP_CONTENT rowLayout.addView( monthHolder.inflateMonthView(rowLayout), LinearLayout.LayoutParams(0, height, 1f), ) } - // todo weight val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT val height = if (daySize.parentDecidesHeight) 0 else WRAP_CONTENT val weight = if (daySize.parentDecidesHeight) 1f else 0f - rootLayout.addView( + monthsLayout.addView( rowLayout, LinearLayout.LayoutParams(width, height, weight), ) return@List row } + run { + val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT + val height = if (daySize.parentDecidesHeight) 0 else WRAP_CONTENT + val weight = if (daySize.parentDecidesHeight) 1f else 0f + rootLayout.addView( + monthsLayout, + LinearLayout.LayoutParams(width, height, weight), + ) + } + val itemFooterView = if (yearItemFooterResource != 0) { rootLayout.inflate(yearItemFooterResource).also { footerView -> rootLayout.addView(footerView) @@ -99,7 +117,10 @@ internal fun setupYearItemRoot( null } - fun setupRoot(root: ViewGroup) { + val itemView = customViewOrRoot( + customViewClass = yearItemViewClass, + 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 { @@ -110,28 +131,6 @@ internal fun setupYearItemRoot( } } - val itemView = yearItemViewClass?.let { - val customLayout = runCatching { - Class.forName(it) - .getDeclaredConstructor(Context::class.java) - .newInstance(rootLayout.context) as ViewGroup - }.onFailure { - Log.e( - "YearCalendarView", - "Failure loading custom class $yearItemViewClass, " + - "check that $yearItemViewClass 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 YearItemContent( itemView = itemView, headerView = itemHeaderView, @@ -140,4 +139,22 @@ internal fun setupYearItemRoot( ) } -internal fun dayTag(date: LocalDate): Int = date.hashCode() +@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 + } + } +} diff --git a/view/src/main/res/values/attrs.xml b/view/src/main/res/values/attrs.xml index d74b56bf..067f68fe 100644 --- a/view/src/main/res/values/attrs.xml +++ b/view/src/main/res/values/attrs.xml @@ -103,6 +103,16 @@ + + + + + + + + + + From 3baf5bee45154d27ba47edb0702f959d814704d8 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 28 Jul 2024 18:03:32 +0200 Subject: [PATCH 04/18] Allow month visibility toggle --- .../calendar/sample/view/Example9Fragment.kt | 10 +++++++--- .../kizitonwose/calendar/view/YearCalendarView.kt | 8 ++++++++ .../calendar/view/internal/MonthHolder.kt | 9 +++++++-- .../internal/yearcalendar/YearCalendarAdapter.kt | 1 + .../view/internal/yearcalendar/YearViewHolder.kt | 13 ++++++++++--- 5 files changed, 33 insertions(+), 8 deletions(-) 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 index 9b4bfa21..dc5f966f 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt @@ -25,6 +25,7 @@ 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 @@ -35,7 +36,6 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, private lateinit var binding: Example9FragmentBinding private var selectedDate: LocalDate? = null - private val today = LocalDate.now() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -47,14 +47,18 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, 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.now(), - Year.now().plusYears(50), + Year.of(currentMonth.year), + Year.of(currentMonth.year).plusYears(50), firstDayOfWeekFromLocale(), ) } diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index f985981d..74b54bc2 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -201,6 +201,14 @@ public open class YearCalendarView : RecyclerView { } } + 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. diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt index 0daadc6a..90b31dce 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt @@ -3,6 +3,7 @@ package com.kizitonwose.calendar.view.internal import android.view.View import android.widget.LinearLayout import androidx.core.view.isGone +import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth @@ -74,7 +75,7 @@ internal class MonthHolder( } fun bindMonthView(month: CalendarMonth) { - monthContainer.itemView.isGone = false + monthContainer.itemView.isVisible = true // The last week row can be empty if out date style is not `EndOfGrid` this.month = month monthContainer.headerView?.let { view -> @@ -94,7 +95,11 @@ internal class MonthHolder( } } - fun hide() { + fun makeInvisible() { + monthContainer.itemView.isInvisible = true + } + + fun makeGone() { monthContainer.itemView.isGone = true } 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 index 3f6b8227..d2a4295d 100644 --- 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 @@ -84,6 +84,7 @@ internal class YearCalendarAdapter( headerView = content.headerView, footerView = content.footerView, monthHolders = content.monthHolders, + isMonthVisible = calView.isMonthVisible, yearHeaderBinder = calView.yearHeaderBinder as YearHeaderFooterBinder?, yearFooterBinder = calView.yearFooterBinder as YearHeaderFooterBinder?, ) 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 index 9a876621..e044c272 100644 --- 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 @@ -4,6 +4,7 @@ import android.view.View import android.view.ViewGroup 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 @@ -17,6 +18,7 @@ internal class YearViewHolder( private val monthHolders: 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 @@ -31,12 +33,17 @@ internal class YearViewHolder( } yearHeaderBinder?.bind(headerContainer, year) } - val months = year.months.filter { true } - monthHolders.flatten().forEachIndexed { index, month -> + val months = year.months.filter(isMonthVisible) + for ((index, month) in monthHolders.flatten().withIndex()) { if (months.size > index) { month.bindMonthView(months[index]) } else { - month.hide() + month.makeInvisible() + } + } + for (row in monthHolders) { + if (row.none(MonthHolder::isShown)) { + row.forEach(MonthHolder::makeGone) } } footerView?.let { view -> From 54d756b9912aef805cb94ff673d295b9b6e3afbe Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 28 Jul 2024 18:28:21 +0200 Subject: [PATCH 05/18] Allow month reload --- .../calendar/sample/view/Example9Fragment.kt | 6 +- .../calendar/view/YearCalendarView.kt | 3 +- .../calendar/view/internal/MonthHolder.kt | 37 +----------- .../yearcalendar/YearCalendarAdapter.kt | 59 ++++++++++++++----- 4 files changed, 53 insertions(+), 52 deletions(-) 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 index dc5f966f..b6376d8d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt @@ -128,6 +128,9 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, } val legendLayout = bind.legendLayout.root } + + val legendTypeface = Typeface.medium(requireContext()) + calendarView.monthHeaderBinder = object : MonthHeaderFooterBinder { override fun create(view: View) = MonthViewContainer(view) @@ -137,13 +140,12 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, if (container.legendLayout.tag == null) { container.legendLayout.tag = true val daysOfWeek = data.weekDays.first().map { it.date.dayOfWeek } - val typeface = Typeface.medium(requireContext()) 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(typeface) + tv.setTypeface(legendTypeface) } } } diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index 74b54bc2..a2394959 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -357,8 +357,7 @@ public open class YearCalendarView : RecyclerView { monthVerticalSpacing, ) monthViewClass = getString(R.styleable.YearCalendarView_cv_monthViewClass) - // TODO - YEAR class - // monthViewClass = getString(R.styleable.YearCalendarView_cv_monthViewClass) + yearViewClass = getString(R.styleable.YearCalendarView_cv_yearViewClass) } check(dayViewResource != 0) { "No value set for `cv_dayViewResource` attribute." } } diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt index 90b31dce..08d14bb7 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt @@ -31,7 +31,6 @@ internal class MonthHolder( fun inflateMonthView(parent: LinearLayout): View { return setupItemRoot( - // TODO - YEAR itemMargins = MarginValues(), daySize = daySize, context = parent.context, @@ -39,39 +38,9 @@ internal class MonthHolder( itemHeaderResource = monthHeaderResource, itemFooterResource = monthFooterResource, weekSize = 6, - // TODO - YEAR - itemViewClass = null, + itemViewClass = monthViewClass, dayBinder = dayBinder as MonthDayBinder, ).also { monthContainer = it }.itemView - -// return LinearLayout(parent.context).apply { -// monthContainer = this -// // TODO - YEAR -//// val width = if (daySize.parentDecidesWidth) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT -//// val height = if (daySize.parentDecidesHeight) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT -//// val weight = if (daySize.parentDecidesHeight) 1f else 0f -// layoutParams = GridLayout.LayoutParams( -// GridLayout.spec( -// /* start = */ GridLayout.UNDEFINED, -// /* size = */ 1, -// /* alignment = */ GridLayout.FILL, -// ), -// GridLayout.spec( -// /* start = */ GridLayout.UNDEFINED, -// /* size = */ 1, -// /* alignment = */ -// if (daySize.parentDecidesHeight) { -// GridLayout.FILL -// } else { -// GridLayout.TOP -// }, -// ), -// ) -// orientation = LinearLayout.VERTICAL -// for (holder in weekHolders) { -// addView(holder.inflateWeekView(this)) -// } -// } } fun bindMonthView(month: CalendarMonth) { @@ -106,8 +75,8 @@ internal class MonthHolder( fun isShown(): Boolean = monthContainer.itemView.isVisible fun reloadMonth(yearMonth: YearMonth): Boolean { - return if (yearMonth == this.month.yearMonth) { - bindMonthView(this.month) + return if (yearMonth == month.yearMonth) { + bindMonthView(month) true } else { false 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 index d2a4295d..bf035bcd 100644 --- 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 @@ -95,7 +95,11 @@ internal class YearCalendarAdapter( super.onBindViewHolder(holder, position, payloads) } else { payloads.forEach { - holder.reloadDay(it as CalendarDay) + when (it) { + is CalendarDay -> holder.reloadDay(it) + is YearMonth -> holder.reloadMonth(it) + else -> {} + } } } } @@ -114,15 +118,10 @@ internal class YearCalendarAdapter( } fun reloadMonth(month: YearMonth) { - // TODO - YEAR - } - - fun findFirstVisibleMonth(): CalendarMonth? { - TODO("Not yet implemented") - } - - fun findLastVisibleMonth(): CalendarMonth? { - TODO("Not yet implemented") + val position = getAdapterPosition(Year.of(month.year)) + if (position != NO_INDEX) { + notifyItemChanged(position, month) + } } fun reloadYear(month: Year) { @@ -199,17 +198,49 @@ internal class YearCalendarAdapter( return if (index == NO_INDEX) null else dataStore[index] } - fun findFirstVisibleDay(): CalendarDay? = findVisibleDay(true) + fun findFirstVisibleMonth(): CalendarMonth? = findVisibleMonth(isFirst = true) + + fun findLastVisibleMonth(): CalendarMonth? = findVisibleMonth(isFirst = false) - fun findLastVisibleDay(): CalendarDay? = findVisibleDay(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() private fun findVisibleDay(isFirst: Boolean): CalendarDay? { - val visibleIndex = - if (isFirst) findFirstVisibleYearPosition() else findLastVisibleYearPosition() + val visibleIndex = if (isFirst) { + findFirstVisibleYearPosition() + } else { + findLastVisibleYearPosition() + } + if (visibleIndex == NO_INDEX) return null + + val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null + val monthRect = Rect() + visibleItemView.getGlobalVisibleRect(monthRect) + // TODO - YEAR + +// val dayRect = Rect() +// return dataStore[visibleIndex].weekDays.flatten() +// .run { if (isFirst) this else reversed() } +// .firstOrNull { +// val dayView = visibleItemView.findViewWithTag(dayTag(it.date)) +// ?: return@firstOrNull false +// dayView.getGlobalVisibleRect(dayRect) +// dayRect.intersect(monthRect) +// } + return null + } + + private fun findVisibleMonth(isFirst: Boolean): CalendarMonth? { + val visibleIndex = if (isFirst) { + findFirstVisibleYearPosition() + } else { + findLastVisibleYearPosition() + } if (visibleIndex == NO_INDEX) return null val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null From c44cf33ecee92ac4bfb29852561602be2ad34462 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 28 Jul 2024 20:02:59 +0200 Subject: [PATCH 06/18] Remove spacing if all row items are hidden. --- .../calendar/view/internal/MonthHolder.kt | 8 +--- .../yearcalendar/YearCalendarAdapter.kt | 2 +- .../view/internal/yearcalendar/YearRoot.kt | 6 +-- .../internal/yearcalendar/YearViewHolder.kt | 41 +++++++++++-------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt index 08d14bb7..db3aa67e 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt @@ -2,7 +2,6 @@ package com.kizitonwose.calendar.view.internal import android.view.View import android.widget.LinearLayout -import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.kizitonwose.calendar.core.CalendarDay @@ -68,11 +67,7 @@ internal class MonthHolder( monthContainer.itemView.isInvisible = true } - fun makeGone() { - monthContainer.itemView.isGone = true - } - - fun isShown(): Boolean = monthContainer.itemView.isVisible + fun isVisible(): Boolean = monthContainer.itemView.isVisible fun reloadMonth(yearMonth: YearMonth): Boolean { return if (yearMonth == month.yearMonth) { @@ -84,5 +79,4 @@ internal class MonthHolder( } fun reloadDay(day: CalendarDay): Boolean = monthContainer.weekHolders.any { it.reloadDay(day) } - } 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 index bf035bcd..4e092bf0 100644 --- 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 @@ -83,7 +83,7 @@ internal class YearCalendarAdapter( rootLayout = content.itemView, headerView = content.headerView, footerView = content.footerView, - monthHolders = content.monthHolders, + monthRowHolders = content.monthRowHolders, isMonthVisible = calView.isMonthVisible, yearHeaderBinder = calView.yearHeaderBinder as YearHeaderFooterBinder?, yearFooterBinder = calView.yearFooterBinder as YearHeaderFooterBinder?, 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 index 524feae0..5315a289 100644 --- 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 @@ -24,7 +24,7 @@ internal data class YearItemContent( val itemView: ViewGroup, val headerView: View?, val footerView: View?, - val monthHolders: List>, + val monthRowHolders: List>>, ) internal fun setupYearItemRoot( @@ -96,7 +96,7 @@ internal fun setupYearItemRoot( rowLayout, LinearLayout.LayoutParams(width, height, weight), ) - return@List row + return@List rowLayout to row } run { @@ -135,7 +135,7 @@ internal fun setupYearItemRoot( itemView = itemView, headerView = itemHeaderView, footerView = itemFooterView, - monthHolders = monthHolders, + monthRowHolders = monthHolders, ) } 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 index e044c272..f45d9c65 100644 --- 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 @@ -2,6 +2,8 @@ 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 @@ -15,7 +17,7 @@ internal class YearViewHolder( rootLayout: ViewGroup, private val headerView: View?, private val footerView: View?, - private val monthHolders: List>, + private val monthRowHolders: List>>, private val yearHeaderBinder: YearHeaderFooterBinder?, private val yearFooterBinder: YearHeaderFooterBinder?, private val isMonthVisible: (month: CalendarMonth) -> Boolean, @@ -34,17 +36,17 @@ internal class YearViewHolder( yearHeaderBinder?.bind(headerContainer, year) } val months = year.months.filter(isMonthVisible) - for ((index, month) in monthHolders.flatten().withIndex()) { - if (months.size > index) { - month.bindMonthView(months[index]) - } else { - month.makeInvisible() - } - } - for (row in monthHolders) { - if (row.none(MonthHolder::isShown)) { - row.forEach(MonthHolder::makeGone) + 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(MonthHolder::isVisible) } footerView?.let { view -> val footerContainer = yearFooterContainer ?: yearFooterBinder!!.create(view).also { @@ -55,14 +57,19 @@ internal class YearViewHolder( } fun reloadMonth(yearMonth: YearMonth) { - monthHolders.flatten() - .filter { it.isShown() } - .firstOrNull { it.reloadMonth(yearMonth) } + visibleItems().firstOrNull { + it.reloadMonth(yearMonth) + } } fun reloadDay(day: CalendarDay) { - monthHolders.flatten() - .filter { it.isShown() } - .firstOrNull { it.reloadDay(day) } + visibleItems().firstOrNull { + it.reloadDay(day) + } } + + private fun visibleItems() = monthRowHolders + .map { it.second } + .flatten() + .filter { it.isVisible() } } From 483f962d44e0c11926b5651c1efb1b9e180a45f0 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 28 Jul 2024 22:25:17 +0200 Subject: [PATCH 07/18] Rename MonthHolder => YearMonthHolder --- .../{MonthHolder.kt => yearcalendar/YearMonthHolder.kt} | 6 ++++-- .../calendar/view/internal/yearcalendar/YearRoot.kt | 5 ++--- .../calendar/view/internal/yearcalendar/YearViewHolder.kt | 5 ++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename view/src/main/java/com/kizitonwose/calendar/view/internal/{MonthHolder.kt => yearcalendar/YearMonthHolder.kt} (93%) diff --git a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearMonthHolder.kt similarity index 93% rename from view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt rename to view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearMonthHolder.kt index db3aa67e..9f50d84b 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/internal/MonthHolder.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearMonthHolder.kt @@ -1,4 +1,4 @@ -package com.kizitonwose.calendar.view.internal +package com.kizitonwose.calendar.view.internal.yearcalendar import android.view.View import android.widget.LinearLayout @@ -11,9 +11,11 @@ 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 MonthHolder( +internal class YearMonthHolder( private val daySize: DaySize, private val dayViewResource: Int, private var dayBinder: MonthDayBinder?, 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 index 5315a289..5943f056 100644 --- 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 @@ -15,7 +15,6 @@ 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.MonthHolder import com.kizitonwose.calendar.view.internal.customViewOrRoot import com.kizitonwose.calendar.view.internal.inflate import kotlin.math.min @@ -24,7 +23,7 @@ internal data class YearItemContent( val itemView: ViewGroup, val headerView: View?, val footerView: View?, - val monthRowHolders: List>>, + val monthRowHolders: List>>, ) internal fun setupYearItemRoot( @@ -72,7 +71,7 @@ internal fun setupYearItemRoot( axisSpacing = monthHorizontalSpacing, ) val row = List(monthColumns) { - MonthHolder( + YearMonthHolder( daySize = daySize, dayViewResource = dayViewResource, dayBinder = dayBinder, 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 index f45d9c65..55731ae7 100644 --- 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 @@ -10,14 +10,13 @@ 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 com.kizitonwose.calendar.view.internal.MonthHolder import java.time.YearMonth internal class YearViewHolder( rootLayout: ViewGroup, private val headerView: View?, private val footerView: View?, - private val monthRowHolders: List>>, + private val monthRowHolders: List>>, private val yearHeaderBinder: YearHeaderFooterBinder?, private val yearFooterBinder: YearHeaderFooterBinder?, private val isMonthVisible: (month: CalendarMonth) -> Boolean, @@ -46,7 +45,7 @@ internal class YearViewHolder( } index += 1 } - rowLayout.isVisible = row.any(MonthHolder::isVisible) + rowLayout.isVisible = row.any(YearMonthHolder::isVisible) } footerView?.let { view -> val footerContainer = yearFooterContainer ?: yearFooterBinder!!.create(view).also { From b261540490df0902686c50f0e421fb3f9f805363 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 29 Jul 2024 10:58:47 +0200 Subject: [PATCH 08/18] Implement findVisibleMonth and findVisibleDay for the year calendar. --- .../view/internal/CalendarLayoutManager.kt | 7 +- .../calendar/view/internal/DayHolder.kt | 10 +- .../calendar/view/internal/Extensions.kt | 23 +++ .../monthcalendar/MonthCalendarAdapter.kt | 35 +++-- .../weekcalendar/WeekCalendarAdapter.kt | 7 +- .../yearcalendar/YearCalendarAdapter.kt | 138 +++++++++--------- .../yearcalendar/YearCalendarLayoutManager.kt | 63 +++++++- .../internal/yearcalendar/YearMonthHolder.kt | 11 +- .../view/internal/yearcalendar/YearRoot.kt | 3 + 9 files changed, 192 insertions(+), 105 deletions(-) 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/DayHolder.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/DayHolder.kt index e3c0f4df..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) } 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..60052609 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,35 @@ 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) + } +} 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..3dd047f8 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,27 @@ 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 = + 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 +210,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 +231,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/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/yearcalendar/YearCalendarAdapter.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/yearcalendar/YearCalendarAdapter.kt index 4e092bf0..2214eacc 100644 --- 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 @@ -2,17 +2,14 @@ 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.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.getCalendarYearData import com.kizitonwose.calendar.data.getYearIndex @@ -23,8 +20,10 @@ 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.Month import java.time.Year import java.time.YearMonth @@ -118,14 +117,14 @@ internal class YearCalendarAdapter( } fun reloadMonth(month: YearMonth) { - val position = getAdapterPosition(Year.of(month.year)) + val position = getAdapterPosition(month) if (position != NO_INDEX) { notifyItemChanged(position, month) } } - fun reloadYear(month: Year) { - notifyItemChanged(getAdapterPosition(month)) + fun reloadYear(year: Year) { + notifyItemChanged(getAdapterPosition(year)) } fun reloadCalendar() { @@ -156,17 +155,7 @@ internal class YearCalendarAdapter( this.visibleYear = visibleYear calView.yearScrollListener?.invoke(visibleYear) - // TODO - YEAR - // Fixes issue where the calendar does not resize its height when in horizontal, paged mode and - // the `outDateStyle` is not `endOfGrid` hence the last row of a 5-row visible month is empty. - // We set such week row's container visibility to GONE in the WeekHolder but it seems the - // RecyclerView accounts for the items in the immediate previous and next indices when - // calculating height and uses the tallest one of the three meaning that the current index's - // view will end up having a blank space at the bottom unless the immediate previous and next - // indices are also missing the last row. I think there should be a better way to fix this. - // New: Also fixes issue where the calendar does not wrap each month's height when in vertical, - // paged mode and just matches parent's height instead. - // Only happens when the CalendarView wraps its height. + // See reason in MonthCalendarAdapter if (calView.scrollPaged && calView.layoutParams.height == WRAP_CONTENT) { val visibleVH = calView.findViewHolderForAdapterPosition(visibleItemPos) ?: return @@ -181,8 +170,12 @@ internal class YearCalendarAdapter( 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.positionYear) + return getAdapterPosition(day.positionYearMonth) } private val layoutManager: YearCalendarLayoutManager @@ -210,54 +203,71 @@ internal class YearCalendarAdapter( 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? { - val visibleIndex = if (isFirst) { - findFirstVisibleYearPosition() - } else { - findLastVisibleYearPosition() - } - if (visibleIndex == NO_INDEX) return null + return visibleMonthInfo(isFirst = isFirst)?.visibleDay(isFirst) + ?: visibleMonthInfo(isFirst, yearIncrement = -1)?.visibleDay(isFirst) + ?: visibleMonthInfo(isFirst, yearIncrement = 1)?.visibleDay(isFirst) + } - val visibleItemView = layoutManager.findViewByPosition(visibleIndex) ?: return null - val monthRect = Rect() - visibleItemView.getGlobalVisibleRect(monthRect) - // TODO - YEAR - -// val dayRect = Rect() -// return dataStore[visibleIndex].weekDays.flatten() -// .run { if (isFirst) this else reversed() } -// .firstOrNull { -// val dayView = visibleItemView.findViewWithTag(dayTag(it.date)) -// ?: return@firstOrNull false -// dayView.getGlobalVisibleRect(dayRect) -// dayRect.intersect(monthRect) -// } - return null + 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? { - val visibleIndex = if (isFirst) { + 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() - visibleItemView.getGlobalVisibleRect(monthRect) - // TODO - YEAR - -// val dayRect = Rect() -// return dataStore[visibleIndex].weekDays.flatten() -// .run { if (isFirst) this else reversed() } -// .firstOrNull { -// val dayView = visibleItemView.findViewWithTag(dayTag(it.date)) -// ?: return@firstOrNull false -// dayView.getGlobalVisibleRect(dayRect) -// dayRect.intersect(monthRect) -// } - return null + 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") @@ -276,21 +286,3 @@ internal class YearCalendarAdapter( notifyDataSetChanged() } } - -// Find the actual year on the calendar where this date is shown. -internal val CalendarDay.positionYear: Year - get() = when (position) { - DayPosition.InDate -> if (date.month == Month.DECEMBER) { - date.yearMonth.nextMonth.year - } else { - date.yearMonth.year - } - - DayPosition.MonthDate -> date.year - DayPosition.OutDate -> if (date.month == Month.JANUARY) { - date.yearMonth.previousMonth.year - } else { - date.yearMonth.year - } - }.let(Year::of) - 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 index 154c03af..44638c5d 100644 --- 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 @@ -1,9 +1,14 @@ 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 @@ -20,10 +25,64 @@ internal class YearCalendarLayoutManager(private val calView: YearCalendarView) override fun scrollPaged(): Boolean = calView.scrollPaged override fun notifyScrollListenerIfNeeded() = adapter.notifyYearScrollListenerIfNeeded() fun smoothScrollToMonth(month: YearMonth) { - // TODO - YEAR + 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) { - // TODO - YEAR + 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 index 9f50d84b..c4feafea 100644 --- 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 @@ -45,9 +45,11 @@ internal class YearMonthHolder( } fun bindMonthView(month: CalendarMonth) { - monthContainer.itemView.isVisible = true - // The last week row can be empty if out date style is not `EndOfGrid` 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 @@ -66,7 +68,10 @@ internal class YearMonthHolder( } fun makeInvisible() { - monthContainer.itemView.isInvisible = true + monthContainer.itemView.apply { + tag = null + isInvisible = true + } } fun isVisible(): Boolean = monthContainer.itemView.isVisible 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 index 5943f056..cad5a91b 100644 --- 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 @@ -17,6 +17,7 @@ import com.kizitonwose.calendar.view.MonthHeaderFooterBinder 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( @@ -157,3 +158,5 @@ private fun DividerLinearLayout( } } } + +internal fun monthTag(month: YearMonth): Int = month.hashCode() From 1035e7c6b304225569ce7d96afd32f0522ba240d Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 29 Jul 2024 15:28:14 +0200 Subject: [PATCH 09/18] Use MarginValues.ZERO where relevant for clarity --- .../main/java/com/kizitonwose/calendar/view/CalendarView.kt | 2 +- .../main/java/com/kizitonwose/calendar/view/MarginValues.kt | 4 ++++ .../java/com/kizitonwose/calendar/view/WeekCalendarView.kt | 2 +- .../java/com/kizitonwose/calendar/view/YearCalendarView.kt | 6 +++--- .../calendar/view/internal/{Utils.kt => ItemRoot.kt} | 0 .../calendar/view/internal/yearcalendar/YearMonthHolder.kt | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) rename view/src/main/java/com/kizitonwose/calendar/view/internal/{Utils.kt => ItemRoot.kt} (100%) 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..c2fec981 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt @@ -165,7 +165,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 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 60160765..877c578f 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/MarginValues.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/MarginValues.kt @@ -17,4 +17,8 @@ public data class MarginValues( end = horizontal, bottom = vertical, ) + + public companion object { + public val ZERO: MarginValues = MarginValues() + } } 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..e51de785 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt @@ -132,7 +132,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 diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index a2394959..cb1f5930 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -263,10 +263,10 @@ public open class YearCalendarView : RecyclerView { } /** - * The margins, in pixels to be applied on each month view. - * this can be used to add a space between two months. + * The margins, in pixels to be applied on each year view. + * this can be used to add a space between two years. */ - public var yearMargins: MarginValues = MarginValues() + public var yearMargins: MarginValues = MarginValues.ZERO set(value) { if (field != value) { field = value 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 100% 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 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 index c4feafea..61b21614 100644 --- 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 @@ -32,7 +32,7 @@ internal class YearMonthHolder( fun inflateMonthView(parent: LinearLayout): View { return setupItemRoot( - itemMargins = MarginValues(), + itemMargins = MarginValues.ZERO, daySize = daySize, context = parent.context, dayViewResource = dayViewResource, From 023959ed9e0ad4a834070fa4ddd9eda5112a2b3a Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 29 Jul 2024 18:18:28 +0200 Subject: [PATCH 10/18] Add `yearBodyMargins` property --- .../kizitonwose/calendar/view/YearCalendarView.kt | 12 ++++++++++++ .../internal/monthcalendar/MonthCalendarAdapter.kt | 7 +++++-- .../internal/yearcalendar/YearCalendarAdapter.kt | 1 + .../view/internal/yearcalendar/YearMonthHolder.kt | 4 ++-- .../calendar/view/internal/yearcalendar/YearRoot.kt | 10 ++++++++-- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index cb1f5930..4b3420c8 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -274,6 +274,18 @@ public open class YearCalendarView : RecyclerView { } } + /** + * The margins, in pixels to be applied on each year body view. + * TODO : IMPROVE with text from compose param to clarify that headers/footers are excluded. + */ + 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) { 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 3dd047f8..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 @@ -195,8 +195,11 @@ internal class MonthCalendarAdapter( } private fun visibleDay(isFirst: Boolean, monthIncrement: Int = 0): CalendarDay? { - var visibleIndex = - if (isFirst) findFirstVisibleMonthPosition() else findLastVisibleMonthPosition() + var visibleIndex = if (isFirst) { + findFirstVisibleMonthPosition() + } else { + findLastVisibleMonthPosition() + } if (visibleIndex == NO_INDEX) return null visibleIndex += monthIncrement 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 index 2214eacc..3dbb29a1 100644 --- 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 @@ -67,6 +67,7 @@ internal class YearCalendarAdapter( monthHorizontalSpacing = calView.monthHorizontalSpacing, monthVerticalSpacing = calView.monthVerticalSpacing, yearItemMargins = calView.yearMargins, + yearBodyMargins = calView.yearBodyMargins, monthHeaderResource = calView.monthHeaderResource, monthFooterResource = calView.monthFooterResource, monthViewClass = calView.monthViewClass, 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 index 61b21614..c51f96dd 100644 --- 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 @@ -22,8 +22,8 @@ internal class YearMonthHolder( private val monthHeaderResource: Int, private val monthFooterResource: Int, private val monthViewClass: String?, - private var monthHeaderBinder: MonthHeaderFooterBinder?, - private var monthFooterBinder: MonthHeaderFooterBinder?, + private val monthHeaderBinder: MonthHeaderFooterBinder?, + private val monthFooterBinder: MonthHeaderFooterBinder?, ) { private lateinit var monthContainer: ItemContent private var headerContainer: ViewContainer? = null 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 index cad5a91b..985de56f 100644 --- 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 @@ -32,6 +32,7 @@ internal fun setupYearItemRoot( monthHorizontalSpacing: Int, monthVerticalSpacing: Int, yearItemMargins: MarginValues, + yearBodyMargins: MarginValues, daySize: DaySize, context: Context, dayViewResource: Int, @@ -64,7 +65,7 @@ internal fun setupYearItemRoot( null } val monthCount = 12 - val rows = (monthCount / monthColumns) + min(1, monthCount % monthColumns) + val rows = (monthCount / monthColumns) + min(monthCount % monthColumns, 1) val monthHolders = List(rows) { val rowLayout = DividerLinearLayout( context = context, @@ -105,7 +106,12 @@ internal fun setupYearItemRoot( val weight = if (daySize.parentDecidesHeight) 1f else 0f rootLayout.addView( monthsLayout, - LinearLayout.LayoutParams(width, height, weight), + LinearLayout.LayoutParams(width, height, weight).apply { + bottomMargin = yearBodyMargins.bottom + topMargin = yearBodyMargins.top + marginStart = yearBodyMargins.start + marginEnd = yearBodyMargins.end + }, ) } From ef3081b52b67f3ecd895918ac437bd4136ad02dc Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 30 Jul 2024 15:43:11 +0200 Subject: [PATCH 11/18] Update year calendar docs --- .../kizitonwose/calendar/view/CalendarView.kt | 51 +++--- .../com/kizitonwose/calendar/view/DaySize.kt | 2 +- .../calendar/view/WeekCalendarView.kt | 63 +++---- .../calendar/view/YearCalendarView.kt | 160 ++++++++++-------- .../calendar/view/internal/Extensions.kt | 2 + 5 files changed, 153 insertions(+), 125 deletions(-) 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 c2fec981..4715af73 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt @@ -10,9 +10,12 @@ 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 @@ -94,8 +97,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] 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**. @@ -330,7 +334,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 +357,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 +373,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 +393,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 +402,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 +411,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 +421,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 +431,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 +462,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 +489,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..ec51766b 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/DaySize.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/DaySize.kt @@ -22,7 +22,7 @@ public enum class DaySize { */ Rectangle, - /** + /** TODO DOC * 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 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 e51de785..ebd77892 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt @@ -7,8 +7,11 @@ 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 @@ -89,8 +92,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] which 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**. @@ -292,7 +296,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 +305,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 +314,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 +347,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 +356,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 +365,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 +375,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 +385,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 +414,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 +440,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 index 4b3420c8..4d0f3a1f 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -13,10 +13,12 @@ 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.monthcalendar.MonthCalendarLayoutManager +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 @@ -36,8 +38,8 @@ public open class YearCalendarView : 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) { @@ -46,8 +48,8 @@ public open class YearCalendarView : 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) { @@ -56,8 +58,8 @@ public open class YearCalendarView : RecyclerView { } /** - * The [YearHeaderFooterBinder] instance used for managing header views. - * The header view is shown above each year on the Calendar. + * The [YearHeaderFooterBinder] instance used for managing the + * header views shown above each year on the Calendar. */ public var yearHeaderBinder: YearHeaderFooterBinder<*>? = null set(value) { @@ -66,8 +68,8 @@ public open class YearCalendarView : RecyclerView { } /** - * The [YearHeaderFooterBinder] instance used for managing footer views. - * The footer view is shown below each year on the Calendar. + * The [YearHeaderFooterBinder] instance used for managing the + * footer views shown below each year on the Calendar. */ public var yearFooterBinder: YearHeaderFooterBinder<*>? = null set(value) { @@ -76,7 +78,7 @@ public open class YearCalendarView : RecyclerView { } /** - * Called when the calendar scrolls to a new month. + * Called when the calendar scrolls to a new year. * Mostly beneficial if [scrollPaged] is `true`. */ public var yearScrollListener: YearScrollListener? = null @@ -88,7 +90,7 @@ public open class YearCalendarView : RecyclerView { public var dayViewResource: Int = 0 set(value) { if (field != value) { - check(value != 0) { "Invalid 'dayViewResource' value." } + require(value != 0) { "Invalid 'dayViewResource' value." } field = value invalidateViewHolders() } @@ -143,8 +145,9 @@ public open class YearCalendarView : RecyclerView { } /** - * A [ViewGroup] which is instantiated and used as the container for each year. - * This class must have a constructor which takes only a [Context]. + * The fully qualified class name of 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**. @@ -153,13 +156,15 @@ public open class YearCalendarView : RecyclerView { set(value) { if (field != value) { field = value + this.javaClass.simpleName invalidateViewHolders() } } /** - * A [ViewGroup] which is instantiated and used as the container for each year. - * This class must have a constructor which takes only a [Context]. + * The fully qualified class name of a [ViewGroup] which 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**. @@ -172,7 +177,9 @@ public open class YearCalendarView : RecyclerView { } } - // TODO - YEAR doc + /** + * The vertical spacing between month rows in each year. + */ @Px public var monthVerticalSpacing: Int = 0 set(value) { @@ -182,6 +189,9 @@ public open class YearCalendarView : RecyclerView { } } + /** + * The horizontal spacing between month columns in each year. + */ @Px public var monthHorizontalSpacing: Int = 0 set(value) { @@ -191,6 +201,9 @@ public open class YearCalendarView : RecyclerView { } } + /** + * 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) { @@ -201,6 +214,10 @@ public open class YearCalendarView : RecyclerView { } } + /** + * 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) { @@ -218,14 +235,14 @@ public open class YearCalendarView : RecyclerView { set(value) { if (field != value) { field = value - (layoutManager as? MonthCalendarLayoutManager)?.orientation = value + (layoutManager as? YearCalendarLayoutManager)?.orientation = value updateSnapHelper() } } /** * The scrolling behavior of the calendar. If `true`, the calendar will - * snap to the nearest month after a scroll or swipe action. + * snap to the nearest year after a scroll or swipe action. * If `false`, the calendar scrolls normally. */ public var scrollPaged: Boolean = false @@ -264,7 +281,8 @@ public open class YearCalendarView : RecyclerView { /** * The margins, in pixels to be applied on each year view. - * this can be used to add a space between two years. + * 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) { @@ -276,7 +294,8 @@ public open class YearCalendarView : RecyclerView { /** * The margins, in pixels to be applied on each year body view. - * TODO : IMPROVE with text from compose param to clarify that headers/footers are excluded. + * 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) { @@ -414,7 +433,7 @@ public open class YearCalendarView : RecyclerView { /** * 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 [smoothScrollToMonth] + * For a smooth scrolling effect, use [smoothScrollToYear] */ public fun scrollToYear(year: Year) { calendarLayoutManager.scrollToIndex(year) @@ -422,7 +441,7 @@ public open class YearCalendarView : RecyclerView { /** * Scroll to a specific year on the calendar using a smooth scrolling animation. - * Just like [scrollToMonth], but with a smooth scrolling animation. + * Just like [scrollToYear], but with a smooth scrolling animation. */ public fun smoothScrollToYear(year: Year) { calendarLayoutManager.smoothScrollToIndex(year) @@ -481,7 +500,7 @@ public open class YearCalendarView : 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. */ @@ -497,14 +516,8 @@ public open class YearCalendarView : RecyclerView { notifyDayChanged(CalendarDay(date, position)) } - // This could replace the other `notifyDateChanged` with one DayPosition param if we add - // the `JvmOverloads` annotation but that would break compatibility in places where the - // method is called with named args: notifyDateChanged(date = *, position = DayPosition.*) - // because assigning single elements to varargs in named form is not allowed. - // 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. */ @@ -520,73 +533,77 @@ public open class YearCalendarView : 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 the view for this [YearMonth] + * Notify the calendar to reload the view for this [year]. + * * 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. + * - [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 CalendarView to reload all months. - * @see [notifyMonthChanged]. + * Notify the calendar to reload all years. + * @see [notifyYearChanged]. */ public fun notifyCalendarChanged() { calendarAdapter.reloadCalendar() } /** - * Find the first visible month on the CalendarView. + * Find the first visible year on the calendar. * - * @return The first visible month or null if not found. + * @return The first visible year or null if not found. */ - public fun findFirstVisibleMonth(): CalendarMonth? { - return calendarAdapter.findFirstVisibleMonth() + public fun findFirstVisibleYear(): CalendarYear? { + return calendarAdapter.findFirstVisibleYear() } /** - * Find the last visible month on the CalendarView. + * Find the last visible year on the calendar. * - * @return The last visible month or null if not found. + * @return The last visible year or null if not found. */ - public fun findLastVisibleMonth(): CalendarMonth? { - return calendarAdapter.findLastVisibleMonth() + public fun findLastVisibleYear(): CalendarYear? { + return calendarAdapter.findLastVisibleYear() } /** - * Find the first visible year on the CalendarView. + * Find the first visible month on the calendar. * - * @return The first visible year or null if not found. + * @return The first visible month or null if not found. */ - public fun findFirstVisibleYear(): CalendarYear? { - return calendarAdapter.findFirstVisibleYear() + public fun findFirstVisibleMonth(): CalendarMonth? { + return calendarAdapter.findFirstVisibleMonth() } /** - * Find the last visible year on the CalendarView. + * Find the last visible month on the calendar. * - * @return The last visible year or null if not found. + * @return The last visible month or null if not found. */ - public fun findLastVisibleYear(): CalendarYear? { - return calendarAdapter.findLastVisibleYear() + public fun findLastVisibleMonth(): CalendarMonth? { + return calendarAdapter.findLastVisibleMonth() } /** - * 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. @@ -596,7 +613,7 @@ public open class YearCalendarView : 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. @@ -606,12 +623,15 @@ public open class YearCalendarView : RecyclerView { } /** - * Setup the CalendarView. - * See [updateMonthData] to update these values. + * Setup the calendar. + * See [updateYearData] to update these values. * - * @param startMonth The first month on the calendar. - * @param endMonth The last month on the calendar. + * @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) @@ -633,13 +653,13 @@ public open class YearCalendarView : 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 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 updateMonthData( + public fun updateYearData( startYear: Year = requireStartYear(), endYear: Year = requireEndYear(), firstDayOfWeek: DayOfWeek = requireFirstDayOfWeek(), @@ -660,13 +680,9 @@ public open class YearCalendarView : RecyclerView { ) } - private fun requireStartYear(): Year = startYear ?: throw getFieldException("startYear") - - private fun requireEndYear(): Year = endYear ?: throw getFieldException("endYear") + private fun requireStartYear(): Year = checkNotNull(startYear) { missingField("startYear") } - private fun requireFirstDayOfWeek(): DayOfWeek = - firstDayOfWeek ?: throw getFieldException("firstDayOfWeek") + private fun requireEndYear(): Year = checkNotNull(endYear) { missingField("endYear") } - 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/internal/Extensions.kt b/view/src/main/java/com/kizitonwose/calendar/view/internal/Extensions.kt index 60052609..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 @@ -33,3 +33,5 @@ internal fun Rect.intersects(other: Rect): Boolean { Rect.intersects(this, other) } } + +internal fun missingField(field: String) = "`$field` is not set. Have you called `setup()`?" From fd1f27612d88f06325df708acc8c3362e26abb77 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 30 Jul 2024 17:46:21 +0200 Subject: [PATCH 12/18] Add MonthHeight year calendar property --- .../kizitonwose/calendar/view/CalendarView.kt | 2 +- .../kizitonwose/calendar/view/MonthHeight.kt | 23 ++++++++ .../calendar/view/WeekCalendarView.kt | 2 +- .../calendar/view/YearCalendarView.kt | 21 +++++++- .../yearcalendar/YearCalendarAdapter.kt | 3 +- .../view/internal/yearcalendar/YearRoot.kt | 54 ++++++++++++------- view/src/main/res/values/attrs.xml | 8 +++ 7 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt 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 4715af73..e1186053 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt @@ -155,7 +155,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) { 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..2968e9d0 --- /dev/null +++ b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt @@ -0,0 +1,23 @@ +package com.kizitonwose.calendar.view + +import android.view.ViewGroup + +/** + * Determines how the height of each month row on the year-based calendar is calculated. + */ +public enum class MonthHeight { + /** TODO DOC + * Each day will have both width and height matching + * the width of the calendar divided by 7. + */ + FollowDaySize, + + /** TODO DOC + * 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 + * or using [ViewGroup.LayoutParams.WRAP_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 ebd77892..21957512 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt @@ -122,7 +122,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) { diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index 4d0f3a1f..89365163 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -269,7 +269,9 @@ public open class YearCalendarView : 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. + * + * @see [monthHeight] */ public var daySize: DaySize = DaySize.Square set(value) { @@ -279,6 +281,20 @@ public open class YearCalendarView : RecyclerView { } } + /** + * 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. @@ -372,6 +388,9 @@ public open class YearCalendarView : RecyclerView { 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), ] 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 index 3dbb29a1..f8f8c818 100644 --- 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 @@ -59,8 +59,9 @@ internal class YearCalendarAdapter( @Suppress("UNCHECKED_CAST") override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YearViewHolder { val content = setupYearItemRoot( - daySize = calView.daySize, context = calView.context, + daySize = calView.daySize, + monthHeight = calView.monthHeight, dayViewResource = calView.dayViewResource, dayBinder = calView.dayBinder as MonthDayBinder, monthColumns = calView.monthColumns, 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 index 985de56f..4e709429 100644 --- 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 @@ -14,6 +14,7 @@ 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 @@ -34,6 +35,7 @@ internal fun setupYearItemRoot( yearItemMargins: MarginValues, yearBodyMargins: MarginValues, daySize: DaySize, + monthHeight: MonthHeight, context: Context, dayViewResource: Int, dayBinder: MonthDayBinder?, @@ -90,30 +92,22 @@ internal fun setupYearItemRoot( LinearLayout.LayoutParams(0, height, 1f), ) } - val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT - val height = if (daySize.parentDecidesHeight) 0 else WRAP_CONTENT - val weight = if (daySize.parentDecidesHeight) 1f else 0f monthsLayout.addView( rowLayout, - LinearLayout.LayoutParams(width, height, weight), + MonthLayoutParams(daySize, monthHeight), ) return@List rowLayout to row } - run { - val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT - val height = if (daySize.parentDecidesHeight) 0 else WRAP_CONTENT - val weight = if (daySize.parentDecidesHeight) 1f else 0f - rootLayout.addView( - monthsLayout, - LinearLayout.LayoutParams(width, height, weight).apply { - bottomMargin = yearBodyMargins.bottom - topMargin = yearBodyMargins.top - marginStart = yearBodyMargins.start - marginEnd = yearBodyMargins.end - }, - ) - } + 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 -> @@ -128,7 +122,10 @@ internal fun setupYearItemRoot( rootLayout = rootLayout, ) { root: ViewGroup -> val width = if (daySize.parentDecidesWidth) MATCH_PARENT else WRAP_CONTENT - val height = if (daySize.parentDecidesHeight) 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 @@ -165,4 +162,23 @@ private fun DividerLinearLayout( } } +@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/res/values/attrs.xml b/view/src/main/res/values/attrs.xml index 067f68fe..5c7c65e8 100644 --- a/view/src/main/res/values/attrs.xml +++ b/view/src/main/res/values/attrs.xml @@ -113,6 +113,14 @@ + + + + + + + + From 7a3ca0817acce5da4b2515aacec303f59e656150 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 30 Jul 2024 20:04:09 +0200 Subject: [PATCH 13/18] Add MonthHeight example for the year calendar --- .../sample/view/CalendarViewOptionsAdapter.kt | 5 + .../calendar/sample/view/Example10Fragment.kt | 179 ++++++++++++++++++ .../calendar/sample/view/Example9Fragment.kt | 1 - .../main/res/layout/example_10_fragment.xml | 38 ++++ sample/src/main/res/menu/example_10_menu.xml | 19 ++ sample/src/main/res/values/strings.xml | 2 + 6 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt create mode 100644 sample/src/main/res/layout/example_10_fragment.xml create mode 100644 sample/src/main/res/menu/example_10_menu.xml 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 eede1885..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 @@ -83,6 +83,11 @@ class CalendarViewOptionsAdapter(val onClick: (ExampleItem) -> Unit) : 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..94c27f99 --- /dev/null +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt @@ -0,0 +1,179 @@ +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.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 { + 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) + setHasOptionsMenu(true) + 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 onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.example_10_menu, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val visibleYear = binding.exTenCalendar.findFirstVisibleYear()?.year + ?: return super.onOptionsItemSelected(item) + return when (item.itemId) { + R.id.menuItemPrevious -> { + binding.exTenCalendar.smoothScrollToYear(visibleYear.minusYears(1)) + true + } + + R.id.menuItemNext -> { + binding.exTenCalendar.smoothScrollToYear(visibleYear.plusYears(1)) + true + } + + else -> super.onOptionsItemSelected(item) + } + } +} 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 index b6376d8d..4563366f 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example9Fragment.kt @@ -39,7 +39,6 @@ class Example9Fragment : BaseFragment(R.layout.example_9_fragment), HasToolbar, override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setHasOptionsMenu(true) binding = Example9FragmentBinding.bind(view) val config = requireContext().resources.configuration val isTablet = config.smallestScreenWidthDp >= 600 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/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 227ff710..c9696921 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -19,6 +19,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 From 030d4cce413225e7fed9d86a4014d89802cd8f73 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 1 Aug 2024 17:51:51 +0200 Subject: [PATCH 14/18] Update docs --- docs/Compose.md | 29 ++++ docs/View.md | 161 +++++++++++++++--- .../kizitonwose/calendar/view/CalendarView.kt | 20 ++- .../com/kizitonwose/calendar/view/DaySize.kt | 13 +- .../kizitonwose/calendar/view/MonthHeight.kt | 33 ++-- .../calendar/view/WeekCalendarView.kt | 20 ++- .../calendar/view/YearCalendarView.kt | 27 +-- view/src/main/res/values/attrs.xml | 25 ++- 8 files changed, 257 insertions(+), 71 deletions(-) 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: Week calendar +## 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: + +Year calendar + 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/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt index e1186053..1810d1c7 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt @@ -22,6 +22,12 @@ 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 @@ -34,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) { @@ -44,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) { @@ -73,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 @@ -85,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 @@ -97,7 +103,7 @@ public open class CalendarView : RecyclerView { } /** - * The fully qualified class name of a [ViewGroup] which is instantiated + * 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]. * 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 ec51766b..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 @@ -22,10 +25,10 @@ public enum class DaySize { */ Rectangle, - /** TODO DOC + /** * 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/MonthHeight.kt b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt index 2968e9d0..50b12911 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt @@ -1,22 +1,33 @@ package com.kizitonwose.calendar.view -import android.view.ViewGroup - /** - * Determines how the height of each month row on the year-based calendar is calculated. + * 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 { - /** TODO DOC - * Each day will have both width and height matching - * the width of the calendar divided by 7. + /** + * 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, - /** TODO DOC - * 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 - * or using [ViewGroup.LayoutParams.WRAP_CONTENT]. + /** + * 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 21957512..578b5629 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt @@ -17,6 +17,12 @@ import com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarLayoutMan 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 @@ -29,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) { @@ -39,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) { @@ -68,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 @@ -80,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 @@ -92,7 +98,7 @@ public open class WeekCalendarView : RecyclerView { } /** - * The fully qualified class name of a [ViewGroup] which is instantiated + * 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]. * diff --git a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt index 89365163..a40f4588 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/YearCalendarView.kt @@ -26,6 +26,12 @@ 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 @@ -39,7 +45,7 @@ public open class YearCalendarView : RecyclerView { /** * The [MonthHeaderFooterBinder] instance used for managing the - * header views shown above each month on the Calendar. + * header views shown above each month on the calendar. */ public var monthHeaderBinder: MonthHeaderFooterBinder<*>? = null set(value) { @@ -49,7 +55,7 @@ public open class YearCalendarView : RecyclerView { /** * The [MonthHeaderFooterBinder] instance used for managing the - * footer views shown below each month on the Calendar. + * footer views shown below each month on the calendar. */ public var monthFooterBinder: MonthHeaderFooterBinder<*>? = null set(value) { @@ -59,7 +65,7 @@ public open class YearCalendarView : RecyclerView { /** * The [YearHeaderFooterBinder] instance used for managing the - * header views shown above each year on the Calendar. + * header views shown above each year on the calendar. */ public var yearHeaderBinder: YearHeaderFooterBinder<*>? = null set(value) { @@ -69,7 +75,7 @@ public open class YearCalendarView : RecyclerView { /** * The [YearHeaderFooterBinder] instance used for managing the - * footer views shown below each year on the Calendar. + * footer views shown below each year on the calendar. */ public var yearFooterBinder: YearHeaderFooterBinder<*>? = null set(value) { @@ -97,7 +103,7 @@ public open class YearCalendarView : 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 @@ -109,7 +115,7 @@ public open class YearCalendarView : 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 @@ -121,7 +127,7 @@ public open class YearCalendarView : RecyclerView { } /** - * The xml resource that is inflated and used as a header for every year. + * The xml resource that is inflated and used as a header for each year. * Set zero to disable. */ public var yearHeaderResource: Int = 0 @@ -133,7 +139,7 @@ public open class YearCalendarView : RecyclerView { } /** - * The xml resource that is inflated and used as a footer for every year. + * The xml resource that is inflated and used as a footer for each year. * Set zero to disable. */ public var yearFooterResource: Int = 0 @@ -145,7 +151,7 @@ public open class YearCalendarView : RecyclerView { } /** - * The fully qualified class name of a [ViewGroup] which is instantiated + * 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]. * @@ -156,13 +162,12 @@ public open class YearCalendarView : RecyclerView { set(value) { if (field != value) { field = value - this.javaClass.simpleName invalidateViewHolders() } } /** - * The fully qualified class name of a [ViewGroup] which is instantiated + * 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]. * diff --git a/view/src/main/res/values/attrs.xml b/view/src/main/res/values/attrs.xml index 5c7c65e8..b9f7094b 100644 --- a/view/src/main/res/values/attrs.xml +++ b/view/src/main/res/values/attrs.xml @@ -104,20 +104,35 @@ - + - + - + - - + + + + From f71c72ba4703e03d05237e3f1beaad8d532a97ec Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 2 Aug 2024 16:52:25 +0200 Subject: [PATCH 15/18] Api dump --- view/api/view.api | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) 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 { +} + From 9a50eeb11561547c233595483b491ef1baa49875 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 3 Aug 2024 09:02:27 +0200 Subject: [PATCH 16/18] Remove deprecated methods in examples --- .../calendar/sample/view/BaseFragment.kt | 11 +++++++++ .../calendar/sample/view/Example10Fragment.kt | 23 ++++++++++--------- .../calendar/sample/view/Example2Fragment.kt | 22 +++++++++--------- .../calendar/sample/view/Example4Fragment.kt | 9 +++++--- 4 files changed, 40 insertions(+), 25 deletions(-) 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/Example10Fragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt index 94c27f99..4da0857f 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example10Fragment.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 androidx.core.view.updatePaddingRelative import com.kizitonwose.calendar.core.CalendarDay @@ -25,7 +26,7 @@ import com.kizitonwose.calendar.view.ViewContainer import java.time.LocalDate import java.time.Year -class Example10Fragment : BaseFragment(R.layout.example_10_fragment), HasToolbar, HasBackButton { +class Example10Fragment : BaseFragment(R.layout.example_10_fragment), HasToolbar, HasBackButton, MenuProvider { override val toolbar: Toolbar get() = binding.exTenToolbar @@ -37,7 +38,6 @@ class Example10Fragment : BaseFragment(R.layout.example_10_fragment), HasToolbar override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setHasOptionsMenu(true) binding = Example10FragmentBinding.bind(view) val config = requireContext().resources.configuration val isTablet = config.smallestScreenWidthDp >= 600 @@ -152,28 +152,29 @@ class Example10Fragment : BaseFragment(R.layout.example_10_fragment), HasToolbar } } } - } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.example_10_menu, menu) + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.example_10_menu, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val visibleYear = binding.exTenCalendar.findFirstVisibleYear()?.year - ?: return super.onOptionsItemSelected(item) + override fun onMenuItemSelected(item: MenuItem): Boolean = with(binding.exTenCalendar) { return when (item.itemId) { R.id.menuItemPrevious -> { - binding.exTenCalendar.smoothScrollToYear(visibleYear.minusYears(1)) + findFirstVisibleYear()?.year?.let { visibleYear -> + smoothScrollToYear(visibleYear.minusYears(1)) + } true } R.id.menuItemNext -> { - binding.exTenCalendar.smoothScrollToYear(visibleYear.plusYears(1)) + findFirstVisibleYear()?.year?.let { visibleYear -> + smoothScrollToYear(visibleYear.plusYears(1)) + } true } - else -> super.onOptionsItemSelected(item) + 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 { From fbc3fb22e38f05e689d2f9cbc10beb344c505008 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 3 Aug 2024 09:02:45 +0200 Subject: [PATCH 17/18] Fix lint --- view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt index 50b12911..b572998e 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/MonthHeight.kt @@ -30,5 +30,4 @@ public enum class MonthHeight { * the day content. */ Fill, - } From b1d6c4f170a9f68b98290fad414da6032f374df3 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 3 Aug 2024 09:26:27 +0200 Subject: [PATCH 18/18] Fix flaky test --- .../kizitonwose/calendar/sample/CalendarViewTest.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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)) }