Skip to content

Commit

Permalink
Selection mode update
Browse files Browse the repository at this point in the history
- add SelectionMode and handle multiply implementations
for all calendar views.
- add YearPicker and MonthPicker as generic views
to select year and month. Add sample of usage in FullDateScreen
- simplify examples
  • Loading branch information
WojciechOsak committed Mar 23, 2024
1 parent 61132b9 commit f9c9f21
Show file tree
Hide file tree
Showing 21 changed files with 431 additions and 167 deletions.
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Features:
| Calendar offsets ||
| Single selection ||
| Multiple selection ||
| Month/Year picker | 🔜 |
| Month/Year picker | |
| Scroll to date animation ||
| Vertical calendar ||
| Range selection | 🔜 |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.wojciechosak.calendar.config

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.wojciechosak.calendar.utils.toMonthYear
Expand All @@ -11,7 +11,7 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.plus

@Stable
class CalendarConfig(
data class CalendarConfig(
val minDate: LocalDate,
val maxDate: LocalDate,
val monthYear: MonthYear,
Expand All @@ -20,6 +20,7 @@ class CalendarConfig(
val showPreviousMonthDays: Boolean,
val showHeader: Boolean,
val showWeekdays: Boolean,
val selectedDates: List<LocalDate>,
)

@Composable
Expand All @@ -33,7 +34,8 @@ fun rememberCalendarState(
showPreviousMonthDays: Boolean = true,
showHeader: Boolean = true,
showWeekdays: Boolean = true,
): State<CalendarConfig> {
selectedDates: MutableList<LocalDate> = mutableListOf(),
): MutableState<CalendarConfig> {
return remember {
mutableStateOf(
CalendarConfig(
Expand All @@ -45,6 +47,7 @@ fun rememberCalendarState(
showPreviousMonthDays = showPreviousMonthDays,
showHeader = showHeader,
showWeekdays = showWeekdays,
selectedDates = selectedDates,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.wojciechosak.calendar.config

sealed class SelectionMode {
data object Single : SelectionMode()

data class Multiply(val bufferSize: Int = 2) : SelectionMode()

data object Range : SelectionMode()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.wojciechosak.calendar.modifiers

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput

@Composable
fun Modifier.passTouchGesture(onTouchEvent: () -> Unit): Modifier =
composed {
pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false)
val change = waitForUpOrCancellation(pass = PointerEventPass.Initial)
change?.let { onTouchEvent() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ internal fun LocalDateTime.toLocalDate(): LocalDate {
return LocalDate(year, month, dayOfMonth)
}

internal fun LocalDate.copy(
fun LocalDate.copy(
year: Int = this.year,
month: Month = this.month,
day: Int = this.dayOfMonth,
): LocalDate {
return LocalDate(year, month, day)
return try {
LocalDate(year, month, day)
} catch (e: IllegalArgumentException) {
LocalDate(year, month, monthLength(month, year))
}
}

fun LocalDate.Companion.today(): LocalDate {
Expand All @@ -50,3 +54,5 @@ fun LocalDate.Companion.today(): LocalDate {
fun LocalDate.toMonthYear(): MonthYear {
return MonthYear(this.month, this.year)
}

fun LocalDate.daySimpleName() = dayOfWeek.name.substring(IntRange(0, 2))
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
Expand All @@ -32,7 +30,7 @@ fun CalendarDay(
) = with(state) {
OutlinedButton(
onClick = onClick,
modifier = modifier.size(50.dp).padding(0.dp),
modifier = modifier,
shape = RoundedCornerShape(50.dp),
border = BorderStroke(1.dp, Color.Transparent),
contentPadding = PaddingValues(0.dp),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.wojciechosak.calendar.view

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -15,6 +16,8 @@ import androidx.compose.ui.unit.sp
import io.wojciechosak.calendar.config.CalendarConfig
import io.wojciechosak.calendar.config.DayState
import io.wojciechosak.calendar.config.MonthYear
import io.wojciechosak.calendar.config.SelectionMode
import io.wojciechosak.calendar.modifiers.passTouchGesture
import io.wojciechosak.calendar.utils.monthLength
import io.wojciechosak.calendar.utils.today
import kotlinx.datetime.DateTimeUnit
Expand All @@ -25,7 +28,7 @@ import kotlinx.datetime.plus

@Composable
fun CalendarView(
config: State<CalendarConfig>,
config: MutableState<CalendarConfig>,
horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceEvenly,
verticalArrangement: Arrangement.Vertical = Arrangement.SpaceEvenly,
isActiveDay: (LocalDate) -> Boolean = { LocalDate.today() == it },
Expand All @@ -36,23 +39,17 @@ fun CalendarView(
MonthHeader(month, year)
},
dayOfWeekLabel: @Composable (dayOfWeek: DayOfWeek) -> Unit = { dayOfWeek ->
val day =
when (dayOfWeek) {
DayOfWeek.MONDAY -> "Mon"
DayOfWeek.TUESDAY -> "Tue"
DayOfWeek.WEDNESDAY -> "Wed"
DayOfWeek.THURSDAY -> "Thu"
DayOfWeek.FRIDAY -> "Fri"
DayOfWeek.SATURDAY -> "Sat"
DayOfWeek.SUNDAY -> "Sun"
else -> ""
}
Text(day, fontSize = 12.sp, textAlign = TextAlign.Center)
Text(
dayOfWeek.name.substring(IntRange(0, 2)),
fontSize = 12.sp,
textAlign = TextAlign.Center,
)
},
selectionMode: SelectionMode = SelectionMode.Multiply(5),
onDateSelected: (List<LocalDate>) -> Unit = {},
modifier: Modifier = Modifier,
) {
val yearMonth by remember { mutableStateOf(config.value.monthYear) }

val daysInCurrentMonth by remember {
mutableStateOf(
monthLength(
Expand Down Expand Up @@ -87,6 +84,7 @@ fun CalendarView(
) {
val state = config.value
val weekDaysCount = if (state.showWeekdays) 7 else 0

items(previousMonthDays + daysInCurrentMonth + nextMonthDays + weekDaysCount) { iteration ->
val isWeekdayLabel = state.showWeekdays && iteration < weekDaysCount
val previousMonthDay =
Expand Down Expand Up @@ -125,18 +123,59 @@ fun CalendarView(
} else if ((!state.showPreviousMonthDays && previousMonthDay) || (!state.showNextMonthDays && nextMonthDay)) {
Text("")
} else {
day(
DayState(
date = newDate,
isActiveDay = isActiveDay(newDate),
isForPreviousMonth = previousMonthDay,
isForNextMonth = nextMonthDay,
enabled = newDate >= state.minDate && newDate <= state.maxDate,
),
)
Box(
modifier =
Modifier.passTouchGesture {
val selectionList = selectDate(date = newDate, mode = selectionMode, list = config.value.selectedDates)
config.value = config.value.copy(selectedDates = selectionList)
onDateSelected(config.value.selectedDates)
},
) {
day(
DayState(
date = newDate,
isActiveDay = isActiveDay(newDate),
isForPreviousMonth = previousMonthDay,
isForNextMonth = nextMonthDay,
enabled = newDate >= state.minDate && newDate <= state.maxDate,
),
)
}
}
}
}
}

private fun selectDate(
date: LocalDate,
mode: SelectionMode,
list: List<LocalDate>,
): List<LocalDate> {
if (list.firstOrNull() == date) return list
val result = mutableListOf<LocalDate>()
result.addAll(list)

when (mode) {
is SelectionMode.Multiply -> {
result.add(0, date)
if (result.size > mode.bufferSize) {
result.removeLast()
}
}

SelectionMode.Range -> {
result.add(0, date)
if (result.size >= 2) {
result.removeLast()
}
}

SelectionMode.Single -> {
result.clear()
result.add(date)
}
}
return result
}

private fun calculateVisibleDaysOfPreviousMonth(monthYear: MonthYear): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fun HorizontalCalendarView(
initialPage = INITIAL_PAGE_INDEX,
pageCount = { Int.MAX_VALUE },
)

HorizontalPager(
state = pagerState,
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
package io.wojciechosak.calendar.view

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import io.wojciechosak.calendar.modifiers.passTouchGesture
import kotlinx.datetime.Month

@Composable
fun MonthPicker() {
}

@Composable
fun MonthView(name: String) {
Text(name)
fun MonthPicker(
columns: Int = 4,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,
verticalArrangement: Arrangement.Vertical = Arrangement.Center,
modifier: Modifier = Modifier,
userScrollEnabled: Boolean = true,
monthCount: Int = 12,
onMonthSelected: (Month) -> Unit = {},
monthView: @Composable (month: Month) -> Unit = { month ->
Text(month.name)
},
) {
require(monthCount in 0..12) {
throw IllegalArgumentException("Month count should be greater than 0 and <= 12!")
}
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
userScrollEnabled = userScrollEnabled,
modifier = modifier,
) {
items(monthCount) { index ->
val selectedMonth = Month.entries.getOrNull(index)
Box(
modifier =
Modifier
.passTouchGesture { selectedMonth?.let { month -> onMonthSelected(month) } },
contentAlignment = Alignment.Center,
) {
selectedMonth?.let { month -> monthView(month) }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import io.wojciechosak.calendar.animation.CalendarAnimator
import io.wojciechosak.calendar.config.CalendarConstants.INITIAL_PAGE_INDEX
import io.wojciechosak.calendar.config.DayState
import io.wojciechosak.calendar.utils.copy
import io.wojciechosak.calendar.utils.daySimpleName
import io.wojciechosak.calendar.utils.monthLength
import io.wojciechosak.calendar.utils.toLocalDate
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.daysUntil
Expand Down Expand Up @@ -50,6 +50,7 @@ fun WeekView(
today == it
},
modifier: Modifier = Modifier,
firstVisibleDate: (LocalDate) -> Unit = {},
day: @Composable (dayState: DayState) -> Unit = { state ->
weekDay(state) {
CalendarDay(
Expand All @@ -76,12 +77,13 @@ fun WeekView(
) {
val index = it - initialPageIndex // week number
calendarAnimator.updatePagerState(pagerState)
for (a in 0..6) {
firstVisibleDate(startDate.plus(index * 7, DateTimeUnit.DAY))
for (day in 0..6) {
Column(
verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val newDate = startDate.plus(index * 7 + a, DateTimeUnit.DAY)
val newDate = startDate.plus(index * 7 + day, DateTimeUnit.DAY)
day(
DayState(
date = newDate,
Expand All @@ -99,17 +101,6 @@ private fun weekDay(
state: DayState,
function: @Composable () -> Unit,
) {
val weekDay =
when (state.date.dayOfWeek) {
DayOfWeek.MONDAY -> "Mon"
DayOfWeek.TUESDAY -> "Tue"
DayOfWeek.WEDNESDAY -> "Wed"
DayOfWeek.THURSDAY -> "Thu"
DayOfWeek.FRIDAY -> "Fri"
DayOfWeek.SATURDAY -> "Sat"
DayOfWeek.SUNDAY -> "Sun"
else -> ""
}
Text(weekDay, fontSize = 12.sp, textAlign = TextAlign.Center)
Text(state.date.daySimpleName(), fontSize = 12.sp, textAlign = TextAlign.Center)
function()
}
Loading

0 comments on commit f9c9f21

Please sign in to comment.