Почти живой календарь

This commit is contained in:
2023-04-04 01:09:26 +03:00
parent 97551f0332
commit 39caedaab0
3 changed files with 154 additions and 41 deletions
+61 -1
View File
@@ -24,10 +24,13 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.compose.CalendarLayoutInfo
import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarLayoutInfo
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.core.*
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import ru.megboyzz.dnevnik.navigation.BaseNavRote import ru.megboyzz.dnevnik.navigation.BaseNavRote
import ru.megboyzz.dnevnik.ui.theme.dark import ru.megboyzz.dnevnik.ui.theme.dark
import java.time.DayOfWeek
@Composable @Composable
fun Int.AsPainter() = painterResource(this) fun Int.AsPainter() = painterResource(this)
@@ -134,6 +137,21 @@ fun rememberFirstMostVisibleMonth(
} }
@Composable
fun rememberFirstMostVisibleWeek(
state: WeekCalendarState,
viewportPercent: Float = 50f,
): Week {
val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleWeek) }
LaunchedEffect(state) {
snapshotFlow { state.layoutInfo.firstMostVisibleWeek(viewportPercent) }
.filterNotNull()
.collect { month -> visibleMonth.value = month }
}
return visibleMonth.value
}
private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50f): CalendarMonth? { private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50f): CalendarMonth? {
return if (visibleMonthsInfo.isEmpty()) { return if (visibleMonthsInfo.isEmpty()) {
null null
@@ -149,3 +167,45 @@ private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50
} }
} }
private fun WeekCalendarLayoutInfo.firstMostVisibleWeek(viewportPercent: Float = 50f): Week? {
return if (visibleWeeksInfo.isEmpty()) {
null
} else {
val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f
visibleWeeksInfo.firstOrNull { itemInfo ->
if (itemInfo.offset < 0) {
itemInfo.offset + itemInfo.size >= viewportSize
} else {
itemInfo.size - itemInfo.offset >= viewportSize
}
}?.week
}
}
fun CalendarDay.week(): Week{
val weekList = mutableListOf<WeekDay>()
val dayOrder = this.date.dayOfWeek.ordinal
var mondayLocalDate = this.date.minusDays(dayOrder.toLong())
DayOfWeek.values().forEach {
weekList.add(WeekDay(mondayLocalDate, WeekDayPosition.RangeDate))
mondayLocalDate = mondayLocalDate.plusDays(1)
}
return Week(weekList)
}
fun WeekDay.week(): Week{
val weekList = mutableListOf<WeekDay>()
val dayOrder = this.date.dayOfWeek.ordinal
var mondayLocalDate = this.date.minusDays(dayOrder.toLong())
DayOfWeek.values().forEach {
weekList.add(WeekDay(mondayLocalDate, WeekDayPosition.RangeDate))
mondayLocalDate = mondayLocalDate.plusDays(1)
}
return Week(weekList)
}
@@ -1,6 +1,5 @@
package ru.megboyzz.dnevnik.screens.ui.calendar package ru.megboyzz.dnevnik.screens.ui.calendar
import android.util.Log
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -12,7 +11,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -28,6 +26,7 @@ import ru.megboyzz.dnevnik.ui.theme.*
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.Month import java.time.Month
import java.time.MonthDay
import java.time.YearMonth import java.time.YearMonth
import java.time.format.TextStyle import java.time.format.TextStyle
import java.util.* import java.util.*
@@ -39,8 +38,9 @@ fun NiceCalendar(
onClick: (day: CalendarDay, month: YearMonth, year: Int) -> Unit onClick: (day: CalendarDay, month: YearMonth, year: Int) -> Unit
) { ) {
val selections = remember { mutableStateListOf<CalendarDay>() } val selections = remember { mutableStateListOf<LocalDate>() }
val daysOfWeek = remember { daysOfWeek() } val daysOfWeek = remember { daysOfWeek() }
var transitionEnabled by remember { mutableStateOf(true) }
val state = rememberCalendarState( val state = rememberCalendarState(
startMonth = startMonth, startMonth = startMonth,
@@ -56,8 +56,11 @@ fun NiceCalendar(
firstDayOfWeek = daysOfWeek.first() firstDayOfWeek = daysOfWeek.first()
) )
var selectedDate by remember { mutableStateOf<LocalDate?>(null) } var selectedWeek by remember { mutableStateOf<Week?>(null) }
val coroutineScope = rememberCoroutineScope()
val monthScrollCoroutineScope = rememberCoroutineScope()
val weekScrollCoroutineScope = rememberCoroutineScope()
val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f) val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f)
Column { Column {
@@ -65,28 +68,49 @@ fun NiceCalendar(
SimpleCalendarTitle( SimpleCalendarTitle(
currentMonth = visibleMonth.yearMonth, currentMonth = visibleMonth.yearMonth,
goToPrevious = { goToPrevious = {
coroutineScope.launch { monthScrollCoroutineScope.launch {
state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth)
} }
}, },
goToNext = { goToNext = {
coroutineScope.launch { monthScrollCoroutineScope.launch {
state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth) state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth)
} }
} },
transitionEnabled = transitionEnabled
) )
Card( Card(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
modifier = Modifier.defaultMinSize(300.dp) modifier = Modifier.defaultMinSize(300.dp)
) { ) {
if(selectedDate == null) { if(selectedWeek == null) {
transitionEnabled = true
HorizontalCalendar( HorizontalCalendar(
state = state, state = state,
dayContent = { day -> dayContent = { day ->
Day(day, isSelected = selectedDate == day.date) { day ->
selectedDate = if (selectedDate == day.date) null else day.date MonthDay(
onClick(day, currentMonth, currentMonth.year) day = day,
isSelected = selections.contains(day.date)
) { clickedDay ->
//Чтобы два раза не вызывать week()
val week = clickedDay.week()
if(!selections.contains(day.date)) {
selections.removeIf { it != day.date }
selections.add(day.date)
}else selections.remove(day.date)
weekScrollCoroutineScope.launch {
weekState.startDate = clickedDay.date
weekState.animateScrollToWeek(clickedDay.date)
}
selectedWeek = if (selectedWeek == week) null else week
onClick(clickedDay, currentMonth, currentMonth.year)
} }
}, },
monthHeader = { DaysOfWeekTitle(daysOfWeek = DayOfWeek.values().asList()) }, monthHeader = { DaysOfWeekTitle(daysOfWeek = DayOfWeek.values().asList()) },
@@ -94,19 +118,20 @@ fun NiceCalendar(
) )
} }
else { else {
//TODO по возможности устранить повторы transitionEnabled = false
WeekCalendar( WeekCalendar(
state = weekState, state = weekState,
weekHeader = { _ -> weekHeader = { _ ->
DaysOfWeekTitle(daysOfWeek = DayOfWeek.values().asList()) DaysOfWeekTitle(daysOfWeek = DayOfWeek.values().asList())
}, },
dayContent = { day -> dayContent = { day ->
Day( WeekDay(
CalendarDay(day.date, DayPosition.InDate), day = CalendarDay(day.date, DayPosition.InDate),
isSelected = selectedDate == day.date isSelected = selections.contains(day.date)
) { calendarDay -> ) { calendarDay ->
selectedDate = if (selectedDate == calendarDay.date) null else calendarDay.date
onClick(calendarDay, currentMonth, currentMonth.year) selectedWeek = null
} }
}, },
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
@@ -122,12 +147,13 @@ fun CalendarNavigationIcon(
icon: Painter, icon: Painter,
contentDescription: String, contentDescription: String,
onClick: () -> Unit, onClick: () -> Unit,
enabled: Boolean = true
) = Box( ) = Box(
modifier = Modifier modifier = Modifier
.size(35.dp) .size(35.dp)
.aspectRatio(1f) .aspectRatio(1f)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.clickable(role = Role.Button, onClick = onClick), .clickable(role = Role.Button, onClick = onClick, enabled = enabled),
) { ) {
Icon( Icon(
modifier = Modifier modifier = Modifier
@@ -146,6 +172,7 @@ fun SimpleCalendarTitle(
currentMonth: YearMonth, currentMonth: YearMonth,
goToPrevious: () -> Unit, goToPrevious: () -> Unit,
goToNext: () -> Unit, goToNext: () -> Unit,
transitionEnabled: Boolean = true
) { ) {
val listOfMonth = listOf( val listOfMonth = listOf(
@@ -164,7 +191,9 @@ fun SimpleCalendarTitle(
) )
Box( Box(
modifier = Modifier.fillMaxWidth().padding(10.dp), modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Row( Row(
@@ -178,6 +207,7 @@ fun SimpleCalendarTitle(
icon = R.drawable.ic_month_swipe_left.AsPainter(), icon = R.drawable.ic_month_swipe_left.AsPainter(),
contentDescription = "Previous", contentDescription = "Previous",
onClick = goToPrevious, onClick = goToPrevious,
enabled = transitionEnabled
) )
Text( Text(
text = currentMonth.displayText(listOfMonth), text = currentMonth.displayText(listOfMonth),
@@ -188,6 +218,7 @@ fun SimpleCalendarTitle(
icon = R.drawable.ic_month_swipe_right.AsPainter(), icon = R.drawable.ic_month_swipe_right.AsPainter(),
contentDescription = "Next", contentDescription = "Next",
onClick = goToNext, onClick = goToNext,
enabled = transitionEnabled
) )
} }
} }
@@ -230,30 +261,57 @@ fun DaysOfWeekTitle(daysOfWeek: List<DayOfWeek>) {
) )
} }
} }
@Composable
fun MonthDay(
day: CalendarDay,
isSelected: Boolean,
onClick: (CalendarDay) -> Unit
) {
Day(day = day, isSelected = isSelected, onClick = onClick)
}
@Composable
fun WeekDay(
day: CalendarDay,
isSelected: Boolean,
onClick: (CalendarDay) -> Unit
) {
val newDay = day.copy(position = DayPosition.MonthDate)
Day(day = newDay, isSelected = isSelected, onClick = onClick)
}
@Composable @Composable
fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) {
Box( Box(
modifier = Modifier modifier = Modifier
.aspectRatio(ratio = 1f) // This is important for square sizing! .aspectRatio(ratio = 1f) // This is important for square sizing!
.clip(CircleShape)
.background(color = if (isSelected) lightGray else Color.Transparent)
.border(
border = BorderStroke(
width = 1.dp,
color = if (isSelected) mainBlue else Color.Transparent
),
shape = CircleShape
)
.clickable( .clickable(
enabled = day.position == DayPosition.MonthDate, enabled = day.position == DayPosition.MonthDate,
onClick = { onClick(day) } onClick = { onClick(day) }
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Box(
text = day.date.dayOfMonth.toString(), modifier = Modifier
style = MainText, .size(30.dp)
color = if (day.position == DayPosition.MonthDate) dark else Color.Gray .clip(CircleShape)
) .background(color = if (isSelected) lightGray else Color.Transparent)
.border(
border = BorderStroke(
width = 1.dp,
color = if (isSelected) mainBlue else Color.Transparent
),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = day.date.dayOfMonth.toString(),
style = MainText,
textAlign = TextAlign.Justify,
color = if (day.position == DayPosition.MonthDate) dark else Color.Gray,
)
}
} }
} }
@@ -1,11 +1,6 @@
package ru.megboyzz.dnevnik package ru.megboyzz.dnevnik
import org.junit.Test
import org.junit.Assert.*
import ru.megboyzz.dnevnik.screens.ui.Month
import java.time.DayOfWeek
class UtilsTest { class UtilsTest {
} }