feat: Implement custom modern popup menu with animations; enhance user interaction and design aesthetics

This commit is contained in:
k1ngsterr1
2026-01-16 23:54:20 +05:00
parent da065ef7f7
commit 1ce7e6498c

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
@@ -51,6 +52,8 @@ 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.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -804,11 +807,9 @@ fun ChatDetailScreen(
} }
} }
// Кнопка меню с выпадающим списком // Кнопка меню - открывает bottom sheet
Box {
IconButton( IconButton(
onClick = { onClick = {
// 🔥 НЕ закрываем клавиатуру при открытии меню
showMenu = true showMenu = true
}, },
modifier = Modifier modifier = Modifier
@@ -822,44 +823,6 @@ fun ChatDetailScreen(
modifier = Modifier.size(26.dp) modifier = Modifier.size(26.dp)
) )
} }
// 🔥 Современное выпадающее меню в стиле iOS/Telegram
ModernPopupMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
isDarkTheme = isDarkTheme
) {
// Block/Unblock User (не показываем для Saved Messages)
if (!isSavedMessages) {
ModernMenuItem(
icon = if (isBlocked) Icons.Default.CheckCircle else Icons.Default.Block,
text = if (isBlocked) "Unblock User" else "Block User",
onClick = {
showMenu = false
if (isBlocked) {
showUnblockConfirm = true
} else {
showBlockConfirm = true
}
},
isDarkTheme = isDarkTheme,
tintColor = PrimaryBlue
)
}
// Delete Chat - деструктивное действие
ModernMenuItem(
icon = Icons.Default.Delete,
text = "Delete Chat",
onClick = {
showMenu = false
showDeleteConfirm = true
},
isDarkTheme = isDarkTheme,
isDestructive = true
)
}
}
} }
} }
} // Закрытие Crossfade } // Закрытие Crossfade
@@ -1435,7 +1398,68 @@ fun ChatDetailScreen(
) )
} }
// 📨 Forward Chat Picker BottomSheet // <EFBFBD> Bottom Sheet меню (вместо popup menu)
if (showMenu) {
ModalBottomSheet(
onDismissRequest = { showMenu = false },
containerColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White,
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
dragHandle = {
Box(
modifier = Modifier
.padding(vertical = 12.dp)
.width(36.dp)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(if (isDarkTheme) Color.White.copy(alpha = 0.3f) else Color.Black.copy(alpha = 0.2f))
)
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp)
) {
// Block/Unblock User
if (!isSavedMessages) {
BottomSheetMenuItem(
icon = if (isBlocked) Icons.Default.CheckCircle else Icons.Default.Block,
text = if (isBlocked) "Unblock User" else "Block User",
onClick = {
showMenu = false
if (isBlocked) {
showUnblockConfirm = true
} else {
showBlockConfirm = true
}
},
isDarkTheme = isDarkTheme,
tintColor = PrimaryBlue
)
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 0.5.dp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
)
}
// Delete Chat
BottomSheetMenuItem(
icon = Icons.Default.Delete,
text = "Delete Chat",
onClick = {
showMenu = false
showDeleteConfirm = true
},
isDarkTheme = isDarkTheme,
isDestructive = true
)
}
}
}
// <20>📨 Forward Chat Picker BottomSheet
if (showForwardPicker) { if (showForwardPicker) {
ForwardChatPickerBottomSheet( ForwardChatPickerBottomSheet(
dialogs = dialogsList, dialogs = dialogsList,
@@ -2628,8 +2652,9 @@ private fun SkeletonBubble(
} }
/** /**
* 🔥 Современное выпадающее меню в стиле iOS/Telegram * 🔥 Современное выпадающее меню в стиле Telegram
* С blur эффектом, красивыми тенями и плавными анимациями * Использует Popup вместо Material DropdownMenu
* С красивыми анимациями как в оригинальном Telegram
*/ */
@Composable @Composable
private fun ModernPopupMenu( private fun ModernPopupMenu(
@@ -2638,64 +2663,75 @@ private fun ModernPopupMenu(
isDarkTheme: Boolean, isDarkTheme: Boolean,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
// Анимация появления if (!expanded) return
val transition = updateTransition(targetState = expanded, label = "menu")
val scale by transition.animateFloat( // Анимация появления в стиле Telegram
transitionSpec = { val scale by animateFloatAsState(
if (targetState) { targetValue = if (expanded) 1f else 0.3f,
spring(dampingRatio = 0.8f, stiffness = 400f) animationSpec = spring(dampingRatio = 0.75f, stiffness = 350f),
} else {
tween(150, easing = FastOutSlowInEasing)
}
},
label = "scale" label = "scale"
) { if (it) 1f else 0.92f } )
val alpha by transition.animateFloat( val alpha by animateFloatAsState(
transitionSpec = { targetValue = if (expanded) 1f else 0f,
if (targetState) tween(200) else tween(100) animationSpec = tween(150, easing = FastOutSlowInEasing),
},
label = "alpha" label = "alpha"
) { if (it) 1f else 0f } )
// Цвета меню // Цвета меню с акцентным оттенком
val menuBackgroundColor = if (isDarkTheme) { val menuBackgroundColor = if (isDarkTheme) {
Color(0xFF2C2C2E) // iOS dark mode menu color Color(0xFF212121) // Telegram dark menu
} else { } else {
Color(0xFFFFFFFF) Color.White
} }
DropdownMenu( val accentBorderColor = PrimaryBlue.copy(alpha = 0.3f) // Тонкая акцентная обводка
expanded = expanded,
Popup(
alignment = Alignment.TopEnd,
offset = IntOffset(-16, 60), // Отступ от кнопки меню
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = PopupProperties(
focusable = true,
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Column(
modifier = Modifier modifier = Modifier
.graphicsLayer { .graphicsLayer {
scaleX = scale scaleX = scale
scaleY = scale scaleY = scale
this.alpha = alpha this.alpha = alpha
transformOrigin = TransformOrigin(0.85f, 0f) transformOrigin = TransformOrigin(1f, 0f) // Анимация от правого верхнего угла
} }
.width(200.dp) .width(220.dp)
.shadow( .shadow(
elevation = 16.dp, elevation = 8.dp,
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(12.dp),
ambientColor = Color.Black.copy(alpha = 0.12f), spotColor = PrimaryBlue.copy(alpha = 0.2f),
spotColor = Color.Black.copy(alpha = 0.08f) ambientColor = PrimaryBlue.copy(alpha = 0.1f)
) )
.clip(RoundedCornerShape(14.dp)) .border(
width = 1.dp,
color = accentBorderColor,
shape = RoundedCornerShape(12.dp)
)
.clip(RoundedCornerShape(12.dp))
.background(menuBackgroundColor) .background(menuBackgroundColor)
.padding(vertical = 8.dp)
) { ) {
content() content()
} }
} }
}
/** /**
* 🔥 Современный элемент меню с иконкой * 🔥 Элемент меню для Bottom Sheet
* Плавные hover эффекты и красивая типографика * Красивый дизайн с большими отступами для удобного нажатия
*/ */
@Composable @Composable
private fun ModernMenuItem( private fun BottomSheetMenuItem(
icon: ImageVector, icon: ImageVector,
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
@@ -2703,7 +2739,7 @@ private fun ModernMenuItem(
tintColor: Color = if (isDarkTheme) Color.White else Color.Black, tintColor: Color = if (isDarkTheme) Color.White else Color.Black,
isDestructive: Boolean = false isDestructive: Boolean = false
) { ) {
val actualTintColor = if (isDestructive) Color(0xFFFF3B30) else tintColor // iOS red val actualTintColor = if (isDestructive) Color(0xFFFF3B30) else tintColor
val textColor = if (isDestructive) { val textColor = if (isDestructive) {
Color(0xFFFF3B30) Color(0xFFFF3B30)
} else if (isDarkTheme) { } else if (isDarkTheme) {
@@ -2712,32 +2748,40 @@ private fun ModernMenuItem(
Color.Black Color.Black
} }
// Hover/pressed состояние // Ripple эффект при нажатии
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
val backgroundColor = if (isPressed.value) {
if (isDarkTheme) Color.White.copy(alpha = 0.08f)
else Color.Black.copy(alpha = 0.04f)
} else {
Color.Transparent
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(backgroundColor)
.clickable( .clickable(
interactionSource = interactionSource, interactionSource = interactionSource,
indication = null indication = null
) { onClick() } ) { onClick() }
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 24.dp, vertical = 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = actualTintColor, tint = actualTintColor,
modifier = Modifier.size(22.dp) modifier = Modifier.size(28.dp)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(20.dp))
Text( Text(
text = text, text = text,
color = textColor, color = textColor,
fontSize = 16.sp, fontSize = 18.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Medium
letterSpacing = (-0.2).sp // iOS стиль
) )
} }
} }