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.offset
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -435,7 +436,10 @@ fun ProfileMetaballOverlay(
// Don't show black metaball shapes when expanded // Don't show black metaball shapes when expanded
val showMetaballLayer = expansionProgress == 0f 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) // LAYER 1: Metaball shapes with blur effect (BLACK shapes only)
// HIDDEN when expanded - only show avatar content // HIDDEN when expanded - only show avatar content
if (showMetaballLayer) { if (showMetaballLayer) {
@@ -548,29 +552,29 @@ fun ProfileMetaballOverlay(
if (isExpanding) { if (isExpanding) {
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// EXPANSION MODE - плавный рост через scale // EXPANSION MODE - TELEGRAM STYLE
// Круг растёт И СРАЗУ превращается в квадрат - ПАРАЛЛЕЛЬНО!
// Без задержки - всё происходит одновременно
// Target size = ширина экрана (квадрат на всю ширину)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
val targetWidth = screenWidthPx val targetSize = screenWidthPx
val targetHeight = headerHeightPx
// Scale от базового размера до целевого // UNIFORM scale - одинаковый по X и Y
val scaleX = lerpFloat(1f, targetWidth / baseSizePx, expansionProgress) val uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
val scaleY = lerpFloat(1f, targetHeight / baseSizePx, expansionProgress)
// Центр смещается к центру экрана/header // Центр смещается к центру header блока
val targetCenterX = screenWidthPx / 2f val targetCenterX = screenWidthPx / 2f
val targetCenterY = headerHeightPx / 2f val targetCenterY = headerHeightPx / 2f
val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress) val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress) val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
// Offset для позиционирования (учитывая что scale от центра) // Offset для позиционирования (scale от центра)
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() } val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() } val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
// Corner radius плавно уменьшается к 0 (квадрат) // Corner radius: круг → квадрат СРАЗУ, без задержки!
// Но только после 70% expansion для эффекта как в Telegram // Telegram: lerp(smallRadius, 0, expandProgress) - напрямую!
val squareProgress = ((expansionProgress - 0.7f) / 0.3f).coerceIn(0f, 1f) val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, squareProgress)
val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() } val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() }
Box( Box(
@@ -579,8 +583,8 @@ fun ProfileMetaballOverlay(
.width(baseSizeDp) .width(baseSizeDp)
.height(baseSizeDp) .height(baseSizeDp)
.graphicsLayer { .graphicsLayer {
this.scaleX = scaleX this.scaleX = uniformScale
this.scaleY = scaleY this.scaleY = uniformScale
alpha = avatarState.opacity alpha = avatarState.opacity
} }
.clip(RoundedCornerShape(cornerRadiusDp)), .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) { if (avatarState.showBlob) {
val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED
val baseSizePx = with(density) { baseSizeDp.toPx() } val baseSizePx = with(density) { baseSizeDp.toPx() }
@@ -688,19 +695,18 @@ fun ProfileMetaballOverlayCompat(
val isExpanding = expansionProgress > 0f val isExpanding = expansionProgress > 0f
if (isExpanding) { if (isExpanding) {
// EXPANSION MODE - scale animation // EXPANSION MODE - scale + corner radius СРАЗУ без задержки
val targetWidth = screenWidthPx // Target size = ширина экрана (квадрат на всю ширину)
val targetHeight = headerHeightPx val targetSize = screenWidthPx
val scaleX = lerpFloat(1f, targetWidth / baseSizePx, expansionProgress) val uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
val scaleY = lerpFloat(1f, targetHeight / baseSizePx, expansionProgress)
val targetCenterX = screenWidthPx / 2f val targetCenterX = screenWidthPx / 2f
val targetCenterY = headerHeightPx / 2f val targetCenterY = headerHeightPx / 2f
val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress) val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress) val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() } val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() } val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
val squareProgress = ((expansionProgress - 0.7f) / 0.3f).coerceIn(0f, 1f) // Corner radius уменьшается СРАЗУ, без порога!
val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, squareProgress) val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() } val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() }
Box( Box(
@@ -709,8 +715,8 @@ fun ProfileMetaballOverlayCompat(
.width(baseSizeDp) .width(baseSizeDp)
.height(baseSizeDp) .height(baseSizeDp)
.graphicsLayer { .graphicsLayer {
this.scaleX = scaleX this.scaleX = uniformScale
this.scaleY = scaleY this.scaleY = uniformScale
alpha = avatarState.opacity alpha = avatarState.opacity
} }
.clip(RoundedCornerShape(cornerRadiusDp)), .clip(RoundedCornerShape(cornerRadiusDp)),

View File

@@ -339,18 +339,19 @@ fun ProfileScreen(
} }
// Telegram: duration = (1 - value) * 250ms при expand, value * 250ms при collapse // Telegram: duration = (1 - value) * 250ms при expand, value * 250ms при collapse
// Но минимизируем для мгновенного отклика
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f) val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
val snapDuration = if (targetOverscroll == maxOverscroll) { val snapDuration = if (targetOverscroll == maxOverscroll) {
((1f - currentProgress) * 250).toInt().coerceIn(100, 300) ((1f - currentProgress) * 150).toInt().coerceIn(50, 150) // Рост - быстро!
} else { } else {
(currentProgress * 250).toInt().coerceIn(100, 300) (currentProgress * 150).toInt().coerceIn(50, 150) // Коллапс - тоже быстро!
} }
val animatedOverscroll by animateFloatAsState( val animatedOverscroll by animateFloatAsState(
targetValue = targetOverscroll, targetValue = targetOverscroll,
animationSpec = tween( animationSpec = tween(
durationMillis = if (isDragging) 0 else snapDuration, durationMillis = if (isDragging) 0 else snapDuration,
easing = FastOutSlowInEasing // Telegram: CubicBezierInterpolator.EASE_BOTH easing = LinearOutSlowInEasing // Быстрый старт, плавное завершение
), ),
label = "overscroll" label = "overscroll"
) )