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