feat: Improve overscroll handling in ProfileScreen with enhanced snapping and state management

This commit is contained in:
k1ngsterr1
2026-01-30 21:40:43 +05:00
parent 489820eae6
commit 5f87f091f7

View File

@@ -312,39 +312,77 @@ fun ProfileScreen(
// Отслеживаем активный drag
var isDragging by remember { mutableStateOf(false) }
// Отслеживаем направление последнего скролла: true = вверх (к кругу), false = вниз (к квадрату)
var lastScrollDirectionUp by remember { mutableStateOf(false) }
// Calculate collapse progress (0 = expanded, 1 = collapsed, negative = overscroll)
val collapseProgress by remember {
derivedStateOf { (animatedScrollOffset.value / maxScrollOffset).coerceIn(-1f, 1f) }
}
// Определяем целевое состояние: true = круг (scrollOffset >= 0), false = квадрат (scrollOffset
// < snapThreshold)
val targetIsCircle by remember {
derivedStateOf {
// Решение: круг если scrollOffset >= snapThreshold (50% от минимального)
scrollOffset >= snapThreshold
}
}
// Текущее отображаемое состояние (для предотвращения мерцания)
var displayedIsCircle by remember { mutableStateOf(true) }
// Overscroll progress - просто на основе scrollOffset
val overscrollProgress by remember {
derivedStateOf {
val progress =
// Если мы в режиме "круг", всегда показываем 0 (полный круг)
// Если в режиме "квадрат", показываем 1 (полный квадрат)
val rawProgress =
if (animatedScrollOffset.value < 0f) {
(-animatedScrollOffset.value / (-minScrollOffset)).coerceIn(0f, 1f)
} else {
0f
}
// Дискретный прогресс на основе displayedIsCircle
val discreteProgress = if (displayedIsCircle) 0f else 1f
Log.d(
TAG,
"📊 overscrollProgress=$progress, scrollOffset=${animatedScrollOffset.value}, minScrollOffset=$minScrollOffset"
"📊 overscrollProgress: raw=$rawProgress, discrete=$discreteProgress, displayedIsCircle=$displayedIsCircle, targetIsCircle=$targetIsCircle, scrollOffset=${animatedScrollOffset.value}"
)
progress
discreteProgress
}
}
// Snap анимация при отпускании
LaunchedEffect(isDragging) {
if (!isDragging && scrollOffset < 0f) {
// Отпустили палец в overscroll зоне - snap к ближайшему состоянию
val targetOffset =
if (scrollOffset < snapThreshold) {
minScrollOffset // Snap к квадрату
} else {
0f // Snap к кругу
}
Log.d(TAG, "🎯 Snap animation: from $scrollOffset to $targetOffset")
// Обновляем displayedIsCircle только при отпускании пальца (snap)
LaunchedEffect(isDragging, targetIsCircle) {
Log.d(
TAG,
"🎯 LaunchedEffect: isDragging=$isDragging, targetIsCircle=$targetIsCircle, displayedIsCircle=$displayedIsCircle, scrollOffset=$scrollOffset, lastScrollDirectionUp=$lastScrollDirectionUp"
)
if (!isDragging) {
// Палец отпущен - snap на основе направления скролла
// Если скроллили ВВЕРХ - snap к кругу, если ВНИЗ - snap к квадрату
val newIsCircle = lastScrollDirectionUp || scrollOffset >= 0f
Log.d(
TAG,
"👆 Finger released: scrollOffset=$scrollOffset, lastScrollDirectionUp=$lastScrollDirectionUp, newIsCircle=$newIsCircle"
)
if (scrollOffset < 0f) {
// Были в overscroll зоне
val targetOffset = if (newIsCircle) 0f else minScrollOffset
Log.d(
TAG,
"🎯 Snap animation: from $scrollOffset to $targetOffset (circle=$newIsCircle)"
)
// Сначала обновляем состояние формы
displayedIsCircle = newIsCircle
// Потом анимируем позицию
animatedScrollOffset.animateTo(
targetValue = targetOffset,
animationSpec =
@@ -352,10 +390,16 @@ fun ProfileScreen(
dampingRatio =
androidx.compose.animation.core.Spring
.DampingRatioMediumBouncy,
stiffness = androidx.compose.animation.core.Spring.StiffnessLow
stiffness =
androidx.compose.animation.core.Spring.StiffnessLow
)
)
scrollOffset = targetOffset
} else if (scrollOffset >= 0f) {
// Обычный скролл - всегда круг
displayedIsCircle = true
animatedScrollOffset.snapTo(scrollOffset)
}
}
}
@@ -363,6 +407,7 @@ fun ProfileScreen(
LaunchedEffect(scrollOffset, isDragging) {
if (isDragging) {
animatedScrollOffset.snapTo(scrollOffset)
Log.d(TAG, "🔄 Sync during drag: scrollOffset=$scrollOffset -> animatedScrollOffset")
}
}
@@ -373,17 +418,40 @@ fun ProfileScreen(
val delta = available.y
val newOffset = scrollOffset - delta
// Отслеживаем, что палец активен
val wasDragging = isDragging
isDragging = true
// Отслеживаем направление скролла (только значимые движения)
if (kotlin.math.abs(delta) > 3f) {
// delta < 0 = палец идёт вверх (скролл контента вниз, возврат к кругу)
// delta > 0 = палец идёт вниз (скролл контента вверх, к квадрату)
lastScrollDirectionUp = delta < 0f
Log.d(
TAG,
"🧭 Direction changed: delta=$delta, lastScrollDirectionUp=$lastScrollDirectionUp"
)
}
Log.d(
TAG,
"🔄 onPreScroll: delta=$delta, scrollOffset=$scrollOffset, newOffset=$newOffset"
"🔄 onPreScroll: delta=$delta, scrollOffset=$scrollOffset, newOffset=$newOffset, wasDragging=$wasDragging, displayedIsCircle=$displayedIsCircle, lastScrollDirectionUp=$lastScrollDirectionUp"
)
// Если скроллим вверх (delta < 0) и были в overscroll - плавно возвращаемся
if (delta < -5f && scrollOffset < 0f) {
Log.d(TAG, "⚡ Scroll up from overscroll: scrollOffset=$scrollOffset")
scrollOffset = (scrollOffset - delta * 0.5f).coerceIn(minScrollOffset, 0f)
// Если скроллим вверх (delta < 0) и были в overscroll зоне - возвращаемся к кругу
if (delta < 0f && scrollOffset < 0f) {
// Плавно уменьшаем overscroll
val newValue = (scrollOffset - delta).coerceAtMost(0f)
scrollOffset = newValue
// Когда достигли 0, переключаемся на круг
if (scrollOffset >= 0f) {
Log.d(TAG, "⚡ Reached 0 from overscroll - switching to circle")
displayedIsCircle = true
scrollOffset = 0f
}
Log.d(TAG, "📤 Scroll up in overscroll: scrollOffset=$scrollOffset")
return Offset(0f, -delta)
}
@@ -413,8 +481,12 @@ fun ProfileScreen(
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
Log.d(
TAG,
"👆 onPostFling: scrollOffset=$scrollOffset, displayedIsCircle=$displayedIsCircle, lastScrollDirectionUp=$lastScrollDirectionUp"
)
isDragging = false
Log.d(TAG, "👆 onPostFling: drag ended, isDragging=false")
Log.d(TAG, "👆 onPostFling: drag ended, isDragging=false, triggering snap")
return super.onPostFling(consumed, available)
}
}