feat: Improve overscroll handling in ProfileScreen with enhanced snapping and state management
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user