Почти живой календарь
This commit is contained in:
@@ -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,11 +261,40 @@ 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!
|
||||||
|
.clickable(
|
||||||
|
enabled = day.position == DayPosition.MonthDate,
|
||||||
|
onClick = { onClick(day) }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(color = if (isSelected) lightGray else Color.Transparent)
|
.background(color = if (isSelected) lightGray else Color.Transparent)
|
||||||
.border(
|
.border(
|
||||||
@@ -243,17 +303,15 @@ fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) {
|
|||||||
color = if (isSelected) mainBlue else Color.Transparent
|
color = if (isSelected) mainBlue else Color.Transparent
|
||||||
),
|
),
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
)
|
|
||||||
.clickable(
|
|
||||||
enabled = day.position == DayPosition.MonthDate,
|
|
||||||
onClick = { onClick(day) }
|
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = day.date.dayOfMonth.toString(),
|
text = day.date.dayOfMonth.toString(),
|
||||||
style = MainText,
|
style = MainText,
|
||||||
color = if (day.position == DayPosition.MonthDate) dark else Color.Gray
|
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 {
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user