From d17c70e871b69c6e96dad13ae14b23dff68bcbb1 Mon Sep 17 00:00:00 2001 From: Agzamus Date: Fri, 18 Sep 2026 01:20:47 +0300 Subject: [PATCH] feat: schedule swiping --- .../vyatsu/ui/schedule/ScheduleScreen.kt | 126 ++++++++++++++++-- 1 file changed, 114 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt b/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt index b76c0fa..8382a72 100644 --- a/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt @@ -25,9 +25,15 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -48,20 +54,27 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.collectLatest import java.time.DayOfWeek import java.time.LocalDate import java.time.format.DateTimeFormatter +import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min val hPadding = 16.dp @@ -128,6 +141,32 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { if (day == null) { Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) } else { + + val schedule = state.schedule!! + val weekDays = state.weekStart + ?.let { ws -> schedule.periods.firstOrNull { it.weekStart == ws } } + ?.let { schedule.daysOf(it) } + .orEmpty() + + val selectedDayIndex = weekDays.indexOfFirst { it.date == state.dayDate }.coerceAtLeast(0) + val pagerState = rememberPagerState( + initialPage = selectedDayIndex, + pageCount = { weekDays.size } + ) + LaunchedEffect(state.dayDate) { + val targetPage = weekDays.indexOfFirst { it.date == state.dayDate } + if (targetPage != -1 && targetPage != pagerState.currentPage) { + pagerState.scrollToPage(targetPage) // Мгновенное переключение без анимации + } + } + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + if (pagerState.isScrollInProgress && page in weekDays.indices) { + viewModel.selectDay(weekDays[page].date) + } + } + } + val isTodayDay = day.date == state.todayDate val dayLabel = if (isTodayDay) "Сегодня " else "" @@ -153,20 +192,31 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { days = weekDays, selectedDate = state.dayDate, todayDate = state.todayDate, - onSelect = viewModel::selectDay + onSelect = viewModel::selectDay, + pagerState = pagerState ) HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding)) - if (day.intervals.all { it.lessons.isEmpty() }) { - Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) - } else { - LazyColumn( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(vertical = 8.dp) - ) { - itemsIndexed(day.intervals) { slotIndex, slot -> - LessonSlot(slotIndex = slotIndex, slot = slot) + + HorizontalPager(state = pagerState, modifier = Modifier.weight(1f)) { page -> + val day = weekDays.getOrNull(page) + + if (day == null || day.intervals.all { it.lessons.isEmpty() }) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + itemsIndexed(day.intervals) { slotIndex, slot -> + LessonSlot(slotIndex = slotIndex, slot = slot) + } } } } @@ -242,14 +292,66 @@ private fun WeekSelector( } } +private fun getVisiblePercentage(item: LazyListItemInfo, layoutInfo: LazyListLayoutInfo): Float { + val viewportStart = layoutInfo.viewportStartOffset + val viewportEnd = layoutInfo.viewportEndOffset + + val itemStart = item.offset + val itemEnd = item.offset + item.size + + val visibleStart = max(itemStart, viewportStart) + val visibleEnd = min(itemEnd, viewportEnd) + val visibleSize = visibleEnd - visibleStart + + return if (visibleSize <= 0) 0f else visibleSize.toFloat() / item.size +} + @Composable private fun DayChips( days: List, selectedDate: LocalDate?, todayDate: LocalDate?, + pagerState: PagerState, onSelect: (LocalDate) -> Unit ) { - LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) { + val listState = rememberLazyListState() + + val density = LocalDensity.current + + val paddingPx = with(density) { hPadding.toPx() } + + + LaunchedEffect(pagerState, days) { + snapshotFlow { + pagerState.settledPage + }.collectLatest { targetPage -> + + if (targetPage in days.indices) { + val layoutInfo = listState.layoutInfo + var visibleItems = layoutInfo.visibleItemsInfo + var targetItem = visibleItems.firstOrNull { it.index == targetPage } + if (targetItem == null) { + listState.scrollToItem(index = targetPage) + val updatedLayout = listState.layoutInfo + visibleItems = updatedLayout.visibleItemsInfo + targetItem = visibleItems.firstOrNull { it.index == targetPage } + } + if (targetItem != null) { + val visiblePercentage = getVisiblePercentage(targetItem, layoutInfo) + val minVisibleThreshold = 0.75f + if (visiblePercentage < minVisibleThreshold) { + val viewPortWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset + val halfViewport = viewPortWidth / 2f + val centerOffset = (halfViewport - (targetItem.size / 2f) - paddingPx).toInt() + listState.animateScrollToItem(index = targetPage, scrollOffset = -centerOffset) + } + } + } + } + } + + + LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) { items(days, key = { it.date.toEpochDay() }) { day -> val selected = selectedDate == day.date val isTodayChip = todayDate == day.date