diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt b/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt index 25f4b74..412ae3c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/metaball/ProfileMetaballOverlay.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -435,7 +436,10 @@ fun ProfileMetaballOverlay( // Don't show black metaball shapes when expanded val showMetaballLayer = expansionProgress == 0f - Box(modifier = modifier.fillMaxSize()) { + Box(modifier = modifier + .fillMaxSize() + .clip(RectangleShape) // Clip to bounds - prevents avatar overflow when expanded + ) { // LAYER 1: Metaball shapes with blur effect (BLACK shapes only) // HIDDEN when expanded - only show avatar content if (showMetaballLayer) { @@ -548,29 +552,29 @@ fun ProfileMetaballOverlay( if (isExpanding) { // ═══════════════════════════════════════════════════════════ - // EXPANSION MODE - плавный рост через scale + // EXPANSION MODE - TELEGRAM STYLE + // Круг растёт И СРАЗУ превращается в квадрат - ПАРАЛЛЕЛЬНО! + // Без задержки - всё происходит одновременно + // Target size = ширина экрана (квадрат на всю ширину) // ═══════════════════════════════════════════════════════════ - val targetWidth = screenWidthPx - val targetHeight = headerHeightPx + val targetSize = screenWidthPx - // Scale от базового размера до целевого - val scaleX = lerpFloat(1f, targetWidth / baseSizePx, expansionProgress) - val scaleY = lerpFloat(1f, targetHeight / baseSizePx, expansionProgress) + // UNIFORM scale - одинаковый по X и Y + val uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress) - // Центр смещается к центру экрана/header + // Центр смещается к центру header блока val targetCenterX = screenWidthPx / 2f val targetCenterY = headerHeightPx / 2f val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress) val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress) - // Offset для позиционирования (учитывая что scale от центра) + // Offset для позиционирования (scale от центра) val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() } val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() } - // Corner radius плавно уменьшается к 0 (квадрат) - // Но только после 70% expansion для эффекта как в Telegram - val squareProgress = ((expansionProgress - 0.7f) / 0.3f).coerceIn(0f, 1f) - val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, squareProgress) + // Corner radius: круг → квадрат СРАЗУ, без задержки! + // Telegram: lerp(smallRadius, 0, expandProgress) - напрямую! + val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress) val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() } Box( @@ -579,8 +583,8 @@ fun ProfileMetaballOverlay( .width(baseSizeDp) .height(baseSizeDp) .graphicsLayer { - this.scaleX = scaleX - this.scaleY = scaleY + this.scaleX = uniformScale + this.scaleY = uniformScale alpha = avatarState.opacity } .clip(RoundedCornerShape(cornerRadiusDp)), @@ -679,7 +683,10 @@ fun ProfileMetaballOverlayCompat( } } - Box(modifier = modifier.fillMaxSize()) { + Box(modifier = modifier + .fillMaxSize() + .clip(RectangleShape) // Clip to bounds - prevents avatar overflow when expanded + ) { if (avatarState.showBlob) { val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED val baseSizePx = with(density) { baseSizeDp.toPx() } @@ -688,19 +695,18 @@ fun ProfileMetaballOverlayCompat( val isExpanding = expansionProgress > 0f if (isExpanding) { - // EXPANSION MODE - scale animation - val targetWidth = screenWidthPx - val targetHeight = headerHeightPx - val scaleX = lerpFloat(1f, targetWidth / baseSizePx, expansionProgress) - val scaleY = lerpFloat(1f, targetHeight / baseSizePx, expansionProgress) + // EXPANSION MODE - scale + corner radius СРАЗУ без задержки + // Target size = ширина экрана (квадрат на всю ширину) + val targetSize = screenWidthPx + val uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress) val targetCenterX = screenWidthPx / 2f val targetCenterY = headerHeightPx / 2f val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress) val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress) val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() } val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() } - val squareProgress = ((expansionProgress - 0.7f) / 0.3f).coerceIn(0f, 1f) - val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, squareProgress) + // Corner radius уменьшается СРАЗУ, без порога! + val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress) val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() } Box( @@ -709,8 +715,8 @@ fun ProfileMetaballOverlayCompat( .width(baseSizeDp) .height(baseSizeDp) .graphicsLayer { - this.scaleX = scaleX - this.scaleY = scaleY + this.scaleX = uniformScale + this.scaleY = uniformScale alpha = avatarState.opacity } .clip(RoundedCornerShape(cornerRadiusDp)), 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 13ecd2b..331f729 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 @@ -339,18 +339,19 @@ fun ProfileScreen( } // Telegram: duration = (1 - value) * 250ms при expand, value * 250ms при collapse + // Но минимизируем для мгновенного отклика val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f) val snapDuration = if (targetOverscroll == maxOverscroll) { - ((1f - currentProgress) * 250).toInt().coerceIn(100, 300) + ((1f - currentProgress) * 150).toInt().coerceIn(50, 150) // Рост - быстро! } else { - (currentProgress * 250).toInt().coerceIn(100, 300) + (currentProgress * 150).toInt().coerceIn(50, 150) // Коллапс - тоже быстро! } val animatedOverscroll by animateFloatAsState( targetValue = targetOverscroll, animationSpec = tween( durationMillis = if (isDragging) 0 else snapDuration, - easing = FastOutSlowInEasing // Telegram: CubicBezierInterpolator.EASE_BOTH + easing = LinearOutSlowInEasing // Быстрый старт, плавное завершение ), label = "overscroll" )