feat: Improve overscroll handling in ProfileScreen with enhanced snapping and state management
This commit is contained in:
@@ -312,50 +312,94 @@ 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")
|
||||
animatedScrollOffset.animateTo(
|
||||
targetValue = targetOffset,
|
||||
animationSpec =
|
||||
spring(
|
||||
dampingRatio =
|
||||
androidx.compose.animation.core.Spring
|
||||
.DampingRatioMediumBouncy,
|
||||
stiffness = androidx.compose.animation.core.Spring.StiffnessLow
|
||||
)
|
||||
// Обновляем 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"
|
||||
)
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user