diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index 746e0a4..700e68c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentActivity @@ -300,16 +301,69 @@ fun ProfileScreen( // Может быть отрицательным для overscroll (оттягивание вверх) var scrollOffset by remember { mutableFloatStateOf(0f) } val maxScrollOffset = expandedHeightPx - collapsedHeightPx - val minScrollOffset = -expandedHeightPx * 0.3f // Максимальный overscroll (30% высоты) + val minScrollOffset = -expandedHeightPx * 0.5f // Максимальный overscroll (50% высоты) + + // Порог для snap - если overscroll > 50%, то snap к квадрату, иначе к кругу + val snapThreshold = minScrollOffset * 0.5f + + // Анимированный overscroll offset для плавного snap + val animatedScrollOffset = remember { androidx.compose.animation.core.Animatable(0f) } + + // Отслеживаем активный drag + var isDragging by remember { mutableStateOf(false) } // Calculate collapse progress (0 = expanded, 1 = collapsed, negative = overscroll) val collapseProgress by remember { - derivedStateOf { (scrollOffset / maxScrollOffset).coerceIn(-1f, 1f) } + derivedStateOf { (animatedScrollOffset.value / maxScrollOffset).coerceIn(-1f, 1f) } } - // Overscroll progress (0 = normal, 1 = full overscroll - квадратный аватар) + // Overscroll progress - просто на основе scrollOffset val overscrollProgress by remember { - derivedStateOf { (-scrollOffset / (-minScrollOffset)).coerceIn(0f, 1f) } + derivedStateOf { + val progress = + if (animatedScrollOffset.value < 0f) { + (-animatedScrollOffset.value / (-minScrollOffset)).coerceIn(0f, 1f) + } else { + 0f + } + Log.d( + TAG, + "📊 overscrollProgress=$progress, scrollOffset=${animatedScrollOffset.value}, minScrollOffset=$minScrollOffset" + ) + progress + } + } + + // 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 + ) + ) + scrollOffset = targetOffset + } + } + + // Синхронизация scrollOffset -> animatedScrollOffset во время drag + LaunchedEffect(scrollOffset, isDragging) { + if (isDragging) { + animatedScrollOffset.snapTo(scrollOffset) + } } // Nested scroll connection for tracking scroll with overscroll support @@ -318,28 +372,51 @@ fun ProfileScreen( override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val delta = available.y val newOffset = scrollOffset - delta + + isDragging = true + + Log.d( + TAG, + "🔄 onPreScroll: delta=$delta, scrollOffset=$scrollOffset, newOffset=$newOffset" + ) + + // Если скроллим вверх (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) + return Offset(0f, -delta) + } + val consumed = when { - // Scroll up (collapse) + // Scroll up (collapse) - delta < 0 = палец идёт вверх delta < 0 && scrollOffset < maxScrollOffset -> { val consumed = (newOffset.coerceIn(minScrollOffset, maxScrollOffset) - scrollOffset) scrollOffset = newOffset.coerceIn(minScrollOffset, maxScrollOffset) + Log.d(TAG, "📈 Collapse: scrollOffset=$scrollOffset") -consumed } - // Scroll down (expand / overscroll) + // Scroll down (expand / overscroll) - delta > 0 = палец идёт вниз delta > 0 && scrollOffset > minScrollOffset -> { val consumed = scrollOffset - newOffset.coerceIn(minScrollOffset, maxScrollOffset) scrollOffset = newOffset.coerceIn(minScrollOffset, maxScrollOffset) + Log.d(TAG, "📉 Expand/Overscroll: scrollOffset=$scrollOffset") consumed } else -> 0f } return Offset(0f, consumed) } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + isDragging = false + Log.d(TAG, "👆 onPostFling: drag ended, isDragging=false") + return super.onPostFling(consumed, available) + } } } @@ -399,9 +476,10 @@ fun ProfileScreen( top = with(density) { // Не увеличиваем padding при overscroll - // (scrollOffset < 0) + // Используем animatedScrollOffset для плавности (expandedHeightPx - - scrollOffset.coerceAtLeast(0f)) + animatedScrollOffset.value + .coerceAtLeast(0f)) .toDp() } ) @@ -714,19 +792,12 @@ private fun CollapsingProfileHeader( val collapseOnly = collapseProgress.coerceAtLeast(0f) if (overscrollProgress > 0f) { - // OVERSCROLL: аватар становится квадратным и занимает весь header - // Header остаётся фиксированным = expandedHeight - avatarWidth = androidx.compose.ui.unit.lerp(circleSize, screenWidthDp, overscrollProgress) - avatarHeight = androidx.compose.ui.unit.lerp(circleSize, expandedHeight, overscrollProgress) - avatarX = - androidx.compose.ui.unit.lerp( - (screenWidthDp - circleSize) / 2, - 0.dp, - overscrollProgress - ) - val avatarCenterY = (expandedHeight - circleSize) / 2 - avatarY = androidx.compose.ui.unit.lerp(avatarCenterY, 0.dp, overscrollProgress) - // Закругление: от круга до квадрата + // OVERSCROLL: размер СРАЗУ полный, закругление плавно уменьшается + avatarWidth = screenWidthDp + avatarHeight = expandedHeight + avatarX = 0.dp + avatarY = 0.dp + // Закругление плавно от круга (circleSize/2) до квадрата (0) cornerRadius = androidx.compose.ui.unit.lerp(circleSize / 2, 0.dp, overscrollProgress) } else { // NORMAL / COLLAPSE: круглый аватар уменьшается при скролле @@ -751,8 +822,8 @@ private fun CollapsingProfileHeader( phase2Progress ) } - // Всегда круглый - cornerRadius = avatarWidth / 2 + // Полностью круглый + cornerRadius = circleSize / 2 } // Для cornerRadius используем меньшую сторону