fix: optimize avatar expansion and collapse animation duration for improved responsiveness in ProfileScreen

This commit is contained in:
k1ngsterr1
2026-02-01 15:33:41 +05:00
parent 6d15a34512
commit 5b983b4a89
2 changed files with 35 additions and 28 deletions

View File

@@ -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)),

View File

@@ -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"
)