From 9fab6501e3ddcf37ecf637ea3c42d3be9638b9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9mence=20Roumy?= Date: Fri, 23 Jun 2023 14:39:22 +0200 Subject: [PATCH] feature: way to change year quickly by clicking on month year text --- .../multidatepickerexample/MainActivity.kt | 1 + .../multidatepicker/MultiDatePicker.kt | 271 +++++++++++------- .../multidatepicker/utils/Dimensions.kt | 1 + 3 files changed, 176 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/com/playmoweb/multidatepickerexample/MainActivity.kt b/app/src/main/java/com/playmoweb/multidatepickerexample/MainActivity.kt index 133ee1c..6c61eee 100644 --- a/app/src/main/java/com/playmoweb/multidatepickerexample/MainActivity.kt +++ b/app/src/main/java/com/playmoweb/multidatepickerexample/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.playmoweb.multidatepicker.MultiDatePicker +import com.playmoweb.multidatepicker.models.MultiDatePickerColors import com.playmoweb.multidatepicker.utils.extensions.ISO8601 import java.text.SimpleDateFormat import java.util.Calendar diff --git a/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/MultiDatePicker.kt b/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/MultiDatePicker.kt index 006b5f2..2a84838 100644 --- a/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/MultiDatePicker.kt +++ b/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/MultiDatePicker.kt @@ -1,11 +1,21 @@ package com.playmoweb.multidatepicker +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.with import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ChevronLeft import androidx.compose.material.icons.rounded.ChevronRight @@ -20,6 +30,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -29,6 +40,7 @@ import com.playmoweb.multidatepicker.utils.extensions.toMonthYear import com.playmoweb.multidatepicker.utils.extensions.toShortDay import java.util.* +@OptIn(ExperimentalAnimationApi::class) @Composable fun MultiDatePicker( modifier: Modifier = Modifier, @@ -39,10 +51,25 @@ fun MultiDatePicker( colors: MultiDatePickerColors = MultiDatePickerColors.defaults(), cardRadius: Dp = mediumRadius, ) { + val localDensity = LocalDensity.current + + val weekDays = listOf(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY) + val allYears = (1900..2100).toList() + val calendar = remember { mutableStateOf(Calendar.getInstance()) } val currDate = remember { mutableStateOf(calendar.value.time) } + val isSelectYear = remember { mutableStateOf(false) } + val yearScrollState = rememberLazyListState( + initialFirstVisibleItemIndex = allYears.indexOf(calendar.value.get(Calendar.YEAR)) - 3, + ) + val pickerHeight = remember { mutableStateOf(0.dp) } - val weekDays = listOf(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY) + LaunchedEffect(isSelectYear.value) { + if (isSelectYear.value) { + val yearIndex = allYears.indexOf(calendar.value.get(Calendar.YEAR)) - 3 + yearScrollState.scrollToItem(yearIndex) + } + } @Composable fun MonthPickerIcon(operation: Operation) { @@ -87,7 +114,10 @@ fun MultiDatePicker( ) { Text( text = currDate.value.toMonthYear(), - style = MaterialTheme.typography.bodyMedium.copy(color = colors.monthColor) + style = MaterialTheme.typography.bodyMedium.copy(color = colors.monthColor), + modifier = Modifier.clickable { + isSelectYear.value = true + } ) Row { @@ -98,109 +128,156 @@ fun MultiDatePicker( } Spacer(Modifier.height(xxsmallPadding)) - /** - * DAYS - */ - Row { - weekDays.map { - Text( - text = Calendar.getInstance().apply { set(Calendar.DAY_OF_WEEK, it) }.time.toShortDay(), - style = MaterialTheme.typography.bodyMedium.copy(color = colors.weekDayColor), - textAlign = TextAlign.Center, - modifier = Modifier.weight(1f / 7f) - ) - } - } - Spacer(Modifier.height(xxsmallPadding)) - - /** - * BODY - */ - Column( - verticalArrangement = Arrangement.spacedBy(xxsmallPadding), - ) { - val daysNumber: IntRange = (1..calendar.value.getActualMaximum(Calendar.DAY_OF_MONTH)) - val days: List = daysNumber.map { calendar.value.apply { set(Calendar.DAY_OF_MONTH, it) }.time } - val daysItem: MutableList = days.toMutableList() - // ADD EMPTY ITEMS TO THE BEGINNING OF THE LIST IF FIRST WEEK DAY OF MONTH DON'T START ON THE FIRST DAY OF THE WEEK - daysItem.first().let { - val dayOfWeek = if (it!!.day == 0) 7 else it.day - (1 until dayOfWeek).forEach { _ -> daysItem.add(0, null) } + AnimatedContent( + isSelectYear.value, + transitionSpec = { + if (targetState) { + fadeIn() with fadeOut() + } else { + fadeIn() with fadeOut() + } } + ) { showYearSelector -> + if (showYearSelector) { + /** + * YEARS SELECTOR + */ + LazyColumn( + modifier = Modifier + .height(pickerHeight.value) + .fillMaxWidth(), + state = yearScrollState, + verticalArrangement = Arrangement.spacedBy(xsmallPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + items(allYears) { year -> + val isSelected = year == calendar.value.get(Calendar.YEAR) - val daysByWeek: List> = daysItem.chunked(7) { it.toMutableList() } - // ADD EMPTY ITEMS TO THE END OF THE LIST IF LAST WEEK DAY OF MONTH DON'T START ON THE FIRST DAY OF THE WEEK - daysByWeek.last().let { (1..7 - it.size).forEach { _ -> daysByWeek.last().add(null) } } - - daysByWeek.map { - Row { - it.map { day -> - val isSelected = day != null && (day == startDate.value || day == endDate.value) - val isBetween = day != null - && startDate.value != null - && endDate.value != null - && (day.after(startDate.value) && day.before(endDate.value)) - val isEnabled = day != null - && (minDate == null || day.after(minDate) || day == minDate) - && (maxDate == null || day.before(maxDate) || day == maxDate) - - val selectedBackgroundColor = animateColorAsState(targetValue = if (isSelected) colors.selectedIndicatorColor else Color.Transparent) - val textColor = animateColorAsState( - targetValue = if (isSelected) { - colors.selectedDayNumberColor - } else if (!isEnabled) { - colors.disableDayColor - } else { - colors.dayNumberColor - } + Text( + year.toString(), + style = if (isSelected) MaterialTheme.typography.headlineMedium.copy(color = colors.monthColor, fontWeight = FontWeight.Black) + else MaterialTheme.typography.titleLarge.copy(color = colors.weekDayColor), + modifier = Modifier + .height(pickerHeight.value / 7 - (if (isSelected) 0.dp else xsmallPadding)) + .clickable { + currDate.value = calendar.value.apply { + set(Calendar.YEAR, year) + }.time + isSelectYear.value = false + } ) + } + } + } else { + Column( + modifier = Modifier.onGloballyPositioned { pickerHeight.value = with(localDensity) { it.size.height.toDp() } } + ) { + /** + * DAYS + */ + Row { + weekDays.map { + Text( + text = Calendar.getInstance().apply { set(Calendar.DAY_OF_WEEK, it) }.time.toShortDay(), + style = MaterialTheme.typography.bodyMedium.copy(color = colors.weekDayColor), + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f / 7f) + ) + } + } + Spacer(Modifier.height(xxsmallPadding)) - Box( - Modifier - .weight(1f / 7f) - .aspectRatio(1f) - .background( - if (isBetween || isSelected && endDate.value != null) colors.selectedDayBackgroundColor else Color.Transparent, - if (isSelected) RoundedCornerShape( - topStartPercent = if (day == startDate.value) 100 else 0, - topEndPercent = if (day == endDate.value) 100 else 0, - bottomEndPercent = if (day == endDate.value) 100 else 0, - bottomStartPercent = if (day == startDate.value) 100 else 0, - ) else RoundedCornerShape(0) - ) - .clip(CircleShape) - .clickable(enabled = isEnabled) { - if (day != null) { - if (startDate.value == null) { - startDate.value = day - } else if (endDate.value == null) { - if (day.before(startDate.value)) startDate.value = day - else if (day.after(startDate.value)) endDate.value = day - else if (day == startDate.value) startDate.value = null + /** + * BODY + */ + Column( + verticalArrangement = Arrangement.spacedBy(xxsmallPadding), + ) { + val daysNumber: IntRange = (1..calendar.value.getActualMaximum(Calendar.DAY_OF_MONTH)) + val days: List = daysNumber.map { calendar.value.apply { set(Calendar.DAY_OF_MONTH, it) }.time } + val daysItem: MutableList = days.toMutableList() + // ADD EMPTY ITEMS TO THE BEGINNING OF THE LIST IF FIRST WEEK DAY OF MONTH DON'T START ON THE FIRST DAY OF THE WEEK + daysItem.first().let { + val dayOfWeek = if (it!!.day == 0) 7 else it.day + (1 until dayOfWeek).forEach { _ -> daysItem.add(0, null) } + } + + val daysByWeek: List> = daysItem.chunked(7) { it.toMutableList() } + // ADD EMPTY ITEMS TO THE END OF THE LIST IF LAST WEEK DAY OF MONTH DON'T START ON THE FIRST DAY OF THE WEEK + daysByWeek.last().let { (1..7 - it.size).forEach { _ -> daysByWeek.last().add(null) } } + + daysByWeek.map { + Row { + it.map { day -> + val isSelected = day != null && (day == startDate.value || day == endDate.value) + val isBetween = day != null + && startDate.value != null + && endDate.value != null + && (day.after(startDate.value) && day.before(endDate.value)) + val isEnabled = day != null + && (minDate == null || day.after(minDate) || day == minDate) + && (maxDate == null || day.before(maxDate) || day == maxDate) + + val selectedBackgroundColor = animateColorAsState(targetValue = if (isSelected) colors.selectedIndicatorColor else Color.Transparent, label = "") + val textColor = animateColorAsState( + targetValue = if (isSelected) { + colors.selectedDayNumberColor + } else if (!isEnabled) { + colors.disableDayColor } else { - startDate.value = day - endDate.value = null + colors.dayNumberColor + }, label = "" + ) + + Box( + Modifier + .weight(1f / 7f) + .aspectRatio(1f) + .background( + if (isBetween || isSelected && endDate.value != null) colors.selectedDayBackgroundColor else Color.Transparent, + if (isSelected) RoundedCornerShape( + topStartPercent = if (day == startDate.value) 100 else 0, + topEndPercent = if (day == endDate.value) 100 else 0, + bottomEndPercent = if (day == endDate.value) 100 else 0, + bottomStartPercent = if (day == startDate.value) 100 else 0, + ) else RoundedCornerShape(0) + ) + .clip(CircleShape) + .clickable(enabled = isEnabled) { + if (day != null) { + if (startDate.value == null) { + startDate.value = day + } else if (endDate.value == null) { + if (day.before(startDate.value)) startDate.value = day + else if (day.after(startDate.value)) endDate.value = day + else if (day == startDate.value) startDate.value = null + } else { + startDate.value = day + endDate.value = null + } + } + } + ) { + Box( + Modifier + .fillMaxSize() + .background(selectedBackgroundColor.value, CircleShape), + contentAlignment = Alignment.Center + ) { + var dayNumber: Int? = null + if (day != null) { + val calendarDay = Calendar.getInstance().apply { time = day } + dayNumber = calendarDay.get(Calendar.DAY_OF_MONTH) + } + + Text( + text = dayNumber?.toString() ?: "", + style = MaterialTheme.typography.bodyMedium.copy(color = textColor.value), + textAlign = TextAlign.Center, + ) } } } - ) { - Box( - Modifier - .fillMaxSize() - .background(selectedBackgroundColor.value, CircleShape), - contentAlignment = Alignment.Center - ) { - var dayNumber: Int? = null - if(day != null) { - val calendarDay = Calendar.getInstance().apply { time = day } - dayNumber = calendarDay.get(Calendar.DAY_OF_MONTH) - } - - Text( - text = dayNumber?.toString() ?: "", - style = MaterialTheme.typography.bodyMedium.copy(color = textColor.value), - textAlign = TextAlign.Center, - ) } } } diff --git a/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/utils/Dimensions.kt b/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/utils/Dimensions.kt index 14f4132..ba8c8d2 100644 --- a/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/utils/Dimensions.kt +++ b/multiDatePicker/src/main/java/com/playmoweb/multidatepicker/utils/Dimensions.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +var xsmallPadding: Dp = 16.dp var xxsmallPadding: Dp = 8.dp var innerPadding: Dp = 20.dp