Skip to content

Commit

Permalink
Added support for min and max month
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinvanmierlo committed Dec 29, 2023
1 parent 3ad64ca commit 62e1ad6
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,17 @@ public fun <T : SelectionState> Calendar(
@Composable
public fun rememberSelectableCalendarState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(10000),
maxMonth: YearMonth = initialMonth.plusMonths(10000),
initialSelection: List<LocalDate> = emptyList(),
initialSelectionMode: SelectionMode = SelectionMode.Single,
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(initialMonth = initialMonth)
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
selectionState: DynamicSelectionState = rememberSaveable(
saver = DynamicSelectionState.Saver(confirmSelectionChange),
Expand All @@ -256,7 +262,13 @@ public fun rememberSelectableCalendarState(
@Composable
public fun rememberCalendarState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(10000),
maxMonth: YearMonth = initialMonth.plusMonths(10000),
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(initialMonth = initialMonth)
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
): CalendarState<EmptySelectionState> = remember { CalendarState(monthState, EmptySelectionState) }
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public fun DefaultMonthHeader(
) {
IconButton(
modifier = Modifier.testTag("Decrement"),
enabled = monthState.currentMonth > monthState.minMonth,
onClick = { monthState.currentMonth = monthState.currentMonth.minusMonths(1) }
) {
Image(
Expand All @@ -57,6 +58,7 @@ public fun DefaultMonthHeader(
Text(text = monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h4)
IconButton(
modifier = Modifier.testTag("Increment"),
enabled = monthState.currentMonth < monthState.maxMonth,
onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) }
) {
Image(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,70 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.setValue
import org.json.JSONObject
import java.time.YearMonth

@Suppress("FunctionName") // Factory function
public fun MonthState(initialMonth: YearMonth): MonthState = MonthStateImpl(initialMonth)
public fun MonthState(
initialMonth: YearMonth,
minMonth: YearMonth,
maxMonth: YearMonth,
): MonthState = MonthStateImpl(initialMonth, minMonth, maxMonth)

@Stable
public interface MonthState {
public var currentMonth: YearMonth
public var minMonth: YearMonth
public var maxMonth: YearMonth

public companion object {
@Suppress("FunctionName") // Factory function
public fun Saver(): Saver<MonthState, String> = Saver(
save = { it.currentMonth.toString() },
restore = { MonthState(YearMonth.parse(it)) }
save = { monthState ->
JSONObject().also { jsonObject ->
jsonObject.put("currentMonth", monthState.currentMonth.toString())
jsonObject.put("minMonth", monthState.minMonth.toString())
jsonObject.put("maxMonth", monthState.maxMonth.toString())
}.toString()
},
restore = {
val jsonObject = JSONObject(it)
MonthState(
YearMonth.parse(jsonObject.getString("currentMonth")),
YearMonth.parse(jsonObject.getString("minMonth")),
YearMonth.parse(jsonObject.getString("maxMonth")),
)
}
)
}
}

@Stable
private class MonthStateImpl(
initialMonth: YearMonth,
minMonth: YearMonth,
maxMonth: YearMonth,
) : MonthState {

private var _currentMonth by mutableStateOf<YearMonth>(initialMonth)
private var _minMonth by mutableStateOf<YearMonth>(minMonth)
private var _maxMonth by mutableStateOf<YearMonth>(maxMonth)

override var currentMonth: YearMonth
get() = _currentMonth
set(value) {
_currentMonth = value
}

override var minMonth: YearMonth
get() = _minMonth
set(value) {
_minMonth = value
}

override var maxMonth: YearMonth
get() = _maxMonth
set(value) {
_maxMonth = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import io.github.boguszpawlowski.composecalendar.week.getWeeks
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit

@OptIn(ExperimentalSnapperApi::class)
@Composable
Expand All @@ -48,8 +49,9 @@ internal fun <T : SelectionState> MonthPager(
) {
val coroutineScope = rememberCoroutineScope()

val initialFirstVisibleItemIndex = remember(initialMonth, monthState.minMonth) { ChronoUnit.MONTHS.between(monthState.minMonth, initialMonth).toInt() }
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = StartIndex,
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val flingBehavior = rememberSnapperFlingBehavior(
lazyListState = listState,
Expand Down Expand Up @@ -77,13 +79,16 @@ internal fun <T : SelectionState> MonthPager(
content = { weekHeader(daysOfWeek) },
)
}
val pagerCount = remember(monthState.minMonth, monthState.maxMonth) {
ChronoUnit.MONTHS.between(monthState.minMonth, monthState.maxMonth).toInt() + 1
}
LazyRow(
modifier = modifier.testTag("MonthPager"),
state = listState,
flingBehavior = flingBehavior,
verticalAlignment = Alignment.Top,
) {
items(PagerItemCount) { index ->
items(pagerCount, key = { monthListState.getMonthForPage(it).let { "${it.year}-${it.monthValue}" } }) { index ->
MonthContent(
modifier = Modifier.fillParentMaxWidth(),
showAdjacentMonths = showAdjacentMonths,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,12 @@ internal class MonthListState(
}

fun getMonthForPage(index: Int): YearMonth =
initialMonth.plusMonths((index - StartIndex).toLong())
monthState.minMonth.plusMonths(index.toLong())

private fun moveToMonth(month: YearMonth) {
if (month == currentFirstVisibleMonth) return
initialMonth.minus(month).let { offset ->
coroutineScope.launch {
listState.animateScrollToItem((StartIndex - offset).toInt())
}
coroutineScope.launch {
listState.animateScrollToItem(ChronoUnit.MONTHS.between(monthState.minMonth, month).toInt())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ private class MonthSelectionState(
@Composable
private fun rememberMonthSelectionState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(10000),
maxMonth: YearMonth = initialMonth.plusMonths(10000),
initialSelection: YearMonth? = null,
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(initialMonth = initialMonth)
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
selectionState: MonthSelectionState = rememberSaveable(saver = MonthSelectionState.Saver()) {
MonthSelectionState(initialSelection = initialSelection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fun MainScreen() {
composable("custom_selection") { CustomSelectionSample() }
composable("viewmodel") { ViewModelSample() }
composable("kotlinx_datetime") { KotlinXDateTimeSample() }
composable("min_max_month") { MinMaxCalendarSample() }
}
}
}
Expand Down Expand Up @@ -103,5 +104,10 @@ fun MainMenu(navController: NavController) {
Button(onClick = { navController.navigate("kotlinx_datetime") }) {
Text(text = "Kotlinx DateTime")
}
Spacer(modifier = Modifier.height(16.dp))

Button(onClick = { navController.navigate("min_max_month") }) {
Text(text = "Min Max Month")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.github.boguszpawlowski.composecalendar.sample

import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.github.boguszpawlowski.composecalendar.CalendarState
import io.github.boguszpawlowski.composecalendar.StaticCalendar
import io.github.boguszpawlowski.composecalendar.rememberCalendarState
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import java.time.YearMonth
import java.time.temporal.ChronoUnit

@Composable
fun MinMaxCalendarSample() {
val calendarState = rememberCalendarState(
initialMonth = YearMonth.now(),
minMonth = YearMonth.now(),
maxMonth = YearMonth.now()
)

Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
StaticCalendar(calendarState = calendarState)

MinMaxControls(calendarState = calendarState)
}
}

@Composable
private fun MinMaxControls(
calendarState: CalendarState<EmptySelectionState>,
) {
Text(
text = "Calendar Min Month",
style = MaterialTheme.typography.h5,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.clickable {
if (calendarState.monthState.minMonth < calendarState.monthState.currentMonth) {
calendarState.monthState.minMonth = calendarState.monthState.minMonth.plusMonths(1L)
}
}
.size(25.dp)
.border(1.dp, Color.Black),
text = "-",
textAlign = TextAlign.Center
)
Text(text = ChronoUnit.MONTHS.between(calendarState.monthState.minMonth, calendarState.monthState.currentMonth).toString())
Text(
modifier = Modifier
.clickable {
calendarState.monthState.minMonth = calendarState.monthState.minMonth.minusMonths(1L)
}
.size(25.dp)
.border(1.dp, Color.Black),
text = "+",
textAlign = TextAlign.Center
)
}

Text(
text = "Calendar Max Month",
style = MaterialTheme.typography.h5,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.clickable {
if (calendarState.monthState.maxMonth > calendarState.monthState.currentMonth) {
calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.plusMonths(1L)
}
}
.size(25.dp)
.border(1.dp, Color.Black),
text = "-",
textAlign = TextAlign.Center
)
Text(text = ChronoUnit.MONTHS.between(calendarState.monthState.currentMonth, calendarState.monthState.maxMonth).toString())
Text(
modifier = Modifier
.clickable {
calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.plusMonths(1L)
}
.size(25.dp)
.border(1.dp, Color.Black),
text = "+",
textAlign = TextAlign.Center
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ fun ViewModelSample() {
val recipes by viewModel.recipesFlow.collectAsState()
val selectedPrice by viewModel.selectedRecipesPriceFlow.collectAsState(0)
val monthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(initialMonth = YearMonth.now())
MonthState(
initialMonth = YearMonth.now(),
minMonth = YearMonth.now().minusMonths(10000),
maxMonth = YearMonth.now().plusMonths(10000),
)
}

LaunchedEffect(monthState) {
Expand Down

0 comments on commit 62e1ad6

Please sign in to comment.