fix: optimize avatar expansion and collapse animation duration for improved responsiveness in ProfileScreen
This commit is contained in:
@@ -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)),
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user