feat: Enhance ProfileScreen with improved collapsing header and back navigation
This commit is contained in:
@@ -3,10 +3,14 @@ package com.rosetta.messenger.ui.settings
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -27,6 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -41,6 +47,8 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private const val TAG = "ProfileScreen"
|
||||||
|
|
||||||
// 🎨 Avatar colors - используем те же цвета что и в ChatsListScreen
|
// 🎨 Avatar colors - используем те же цвета что и в ChatsListScreen
|
||||||
private val avatarColorsLight =
|
private val avatarColorsLight =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -103,7 +111,7 @@ private fun getInitials(name: String): String {
|
|||||||
// 🎯 COLLAPSING HEADER CONSTANTS
|
// 🎯 COLLAPSING HEADER CONSTANTS
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
private val EXPANDED_HEADER_HEIGHT = 280.dp
|
private val EXPANDED_HEADER_HEIGHT = 280.dp
|
||||||
private val COLLAPSED_HEADER_HEIGHT = 56.dp
|
private val COLLAPSED_HEADER_HEIGHT = 64.dp // Increased for more bottom space
|
||||||
private val AVATAR_SIZE_EXPANDED = 120.dp
|
private val AVATAR_SIZE_EXPANDED = 120.dp
|
||||||
private val AVATAR_SIZE_COLLAPSED = 36.dp
|
private val AVATAR_SIZE_COLLAPSED = 36.dp
|
||||||
private val STATUS_BAR_HEIGHT = 24.dp
|
private val STATUS_BAR_HEIGHT = 24.dp
|
||||||
@@ -141,8 +149,9 @@ fun ProfileScreen(
|
|||||||
|
|
||||||
// Scroll state for collapsing header animation
|
// Scroll state for collapsing header animation
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val expandedHeightPx = with(density) { EXPANDED_HEADER_HEIGHT.toPx() }
|
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + STATUS_BAR_HEIGHT).toPx() }
|
val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT + statusBarHeight).toPx() }
|
||||||
|
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + statusBarHeight).toPx() }
|
||||||
|
|
||||||
// Track scroll offset with animated state for smooth transitions
|
// Track scroll offset with animated state for smooth transitions
|
||||||
var scrollOffset by remember { mutableFloatStateOf(0f) }
|
var scrollOffset by remember { mutableFloatStateOf(0f) }
|
||||||
@@ -426,36 +435,46 @@ private fun CollapsingProfileHeader(
|
|||||||
isDarkTheme: Boolean
|
isDarkTheme: Boolean
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenWidthDp = configuration.screenWidthDp.dp
|
||||||
|
|
||||||
// Animated values using lerp
|
// Get actual status bar height
|
||||||
val headerHeight = androidx.compose.ui.unit.lerp(
|
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
EXPANDED_HEADER_HEIGHT,
|
|
||||||
COLLAPSED_HEADER_HEIGHT + STATUS_BAR_HEIGHT,
|
|
||||||
collapseProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
val avatarSize = androidx.compose.ui.unit.lerp(
|
// Header heights
|
||||||
AVATAR_SIZE_EXPANDED,
|
val expandedHeight = EXPANDED_HEADER_HEIGHT + statusBarHeight
|
||||||
AVATAR_SIZE_COLLAPSED,
|
val collapsedHeight = COLLAPSED_HEADER_HEIGHT + statusBarHeight
|
||||||
collapseProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
// Avatar position - smooth transition from center to left
|
// Animated header height
|
||||||
val screenWidthDp = with(density) { 360.dp } // approximate
|
val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress)
|
||||||
val avatarStartX = (screenWidthDp - AVATAR_SIZE_EXPANDED) / 2
|
|
||||||
val avatarEndX = 56.dp // After back button
|
|
||||||
|
|
||||||
val avatarX = androidx.compose.ui.unit.lerp(avatarStartX, avatarEndX, collapseProgress)
|
// ═══════════════════════════════════════════════════════════
|
||||||
val avatarY = androidx.compose.ui.unit.lerp(70.dp, STATUS_BAR_HEIGHT + 10.dp, collapseProgress)
|
// 👤 AVATAR - shrinks and moves UP until disappears
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED) / 2
|
||||||
|
val avatarStartY = statusBarHeight + 32.dp
|
||||||
|
val avatarEndY = statusBarHeight - 60.dp // Moves above screen
|
||||||
|
val avatarY = androidx.compose.ui.unit.lerp(avatarStartY, avatarEndY, collapseProgress)
|
||||||
|
val avatarSize = androidx.compose.ui.unit.lerp(AVATAR_SIZE_EXPANDED, 0.dp, collapseProgress)
|
||||||
|
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress)
|
||||||
|
|
||||||
// Text alpha animations
|
// ═══════════════════════════════════════════════════════════
|
||||||
val expandedContentAlpha = (1f - collapseProgress * 2f).coerceIn(0f, 1f) // Fades out faster
|
// 📝 TEXT - moves from center to left corner
|
||||||
val collapsedContentAlpha = ((collapseProgress - 0.5f) * 2f).coerceIn(0f, 1f) // Fades in at 50%
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val textExpandedX = screenWidthDp / 2
|
||||||
|
val textCollapsedX = 48.dp + 12.dp // After back button + gap (left aligned)
|
||||||
|
val textX = androidx.compose.ui.unit.lerp(textExpandedX, textCollapsedX, collapseProgress)
|
||||||
|
|
||||||
// Avatar scale for "drop" effect - shrinks faster at the end
|
val textExpandedY = statusBarHeight + 32.dp + AVATAR_SIZE_EXPANDED + 48.dp
|
||||||
val avatarScaleMultiplier = if (collapseProgress > 0.7f) {
|
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
|
||||||
1f - ((collapseProgress - 0.7f) / 0.3f) * 0.15f
|
val textY = androidx.compose.ui.unit.lerp(textExpandedY, textCollapsedY, collapseProgress)
|
||||||
} else 1f
|
|
||||||
|
// Font sizes
|
||||||
|
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress)
|
||||||
|
val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress)
|
||||||
|
|
||||||
|
// Text alignment interpolation (0 = center, 1 = left)
|
||||||
|
val textCenterOffset = androidx.compose.ui.unit.lerp(80.dp, 0.dp, collapseProgress)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -466,123 +485,90 @@ private fun CollapsingProfileHeader(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 👤 ANIMATED AVATAR - Telegram "drop" effect
|
// 🔙 BACK BUTTON
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = avatarX, y = avatarY)
|
.padding(top = statusBarHeight)
|
||||||
.size(avatarSize)
|
.padding(start = 4.dp, top = 4.dp)
|
||||||
.graphicsLayer {
|
.size(48.dp),
|
||||||
scaleX = avatarScaleMultiplier
|
contentAlignment = Alignment.Center
|
||||||
scaleY = avatarScaleMultiplier
|
) {
|
||||||
// Subtle alpha decrease at the very end for smooth collapse
|
IconButton(
|
||||||
alpha = if (collapseProgress > 0.9f) {
|
onClick = onBack,
|
||||||
1f - ((collapseProgress - 0.9f) / 0.1f) * 0.2f
|
modifier = Modifier.size(48.dp)
|
||||||
} else 1f
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 👤 AVATAR - shrinks and moves up
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
if (avatarSize > 1.dp) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(
|
||||||
|
x = avatarCenterX + (AVATAR_SIZE_EXPANDED - avatarSize) / 2,
|
||||||
|
y = avatarY
|
||||||
|
)
|
||||||
|
.size(avatarSize)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color.White.copy(alpha = 0.2f))
|
.background(Color.White.copy(alpha = 0.15f))
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(avatarColors.backgroundColor),
|
.background(avatarColors.backgroundColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
if (avatarFontSize > 1.sp) {
|
||||||
Text(
|
Text(
|
||||||
text = getInitials(name),
|
text = getInitials(name),
|
||||||
fontSize = androidx.compose.ui.unit.lerp(40.sp, 14.sp, collapseProgress),
|
fontSize = avatarFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = avatarColors.textColor
|
color = avatarColors.textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 📝 EXPANDED STATE - Name and info below avatar
|
// 📝 TEXT BLOCK - Name + Online, moves to corner, left-aligned when collapsed
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.offset(x = textX - textCenterOffset, y = textY)
|
||||||
.padding(top = 200.dp)
|
.graphicsLayer {
|
||||||
.graphicsLayer { alpha = expandedContentAlpha },
|
val centerOffsetY = with(density) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
androidx.compose.ui.unit.lerp(24.dp, 18.dp, collapseProgress).toPx()
|
||||||
|
}
|
||||||
|
translationY = -centerOffsetY
|
||||||
|
},
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
) {
|
) {
|
||||||
// Name
|
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = name,
|
||||||
fontSize = 24.sp,
|
fontSize = nameFontSize,
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
// Username and public key
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
if (username.isNotBlank()) {
|
|
||||||
Text(
|
|
||||||
text = "@$username",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.White.copy(alpha = 0.8f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = " • ",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.White.copy(alpha = 0.8f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = "${publicKey.take(3)}...${publicKey.takeLast(3)}",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.White.copy(alpha = 0.8f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
// 📝 COLLAPSED STATE - Toolbar with name next to avatar
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.statusBarsPadding()
|
|
||||||
.height(COLLAPSED_HEADER_HEIGHT)
|
|
||||||
.padding(start = 100.dp) // Space for back button + avatar
|
|
||||||
.graphicsLayer { alpha = collapsedContentAlpha },
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = name,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.widthIn(max = 220.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
|
// Online text - centered in expanded, left in collapsed
|
||||||
|
val onlineCenterOffset = androidx.compose.ui.unit.lerp(65.dp, 0.dp, collapseProgress)
|
||||||
Text(
|
Text(
|
||||||
text = "online",
|
text = "online",
|
||||||
fontSize = 13.sp,
|
fontSize = onlineFontSize,
|
||||||
color = Color.White.copy(alpha = 0.7f)
|
color = Color(0xFF4CAF50),
|
||||||
)
|
modifier = Modifier.offset(x = onlineCenterOffset)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
// 🔙 BACK BUTTON - Always visible
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack,
|
|
||||||
modifier = Modifier
|
|
||||||
.statusBarsPadding()
|
|
||||||
.padding(4.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = Color.White
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +581,7 @@ private fun CollapsingProfileHeader(
|
|||||||
exit = fadeOut() + shrinkHorizontally(),
|
exit = fadeOut() + shrinkHorizontally(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.statusBarsPadding()
|
.padding(top = statusBarHeight)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onSave) {
|
TextButton(onClick = onSave) {
|
||||||
|
|||||||
Reference in New Issue
Block a user