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,61 +807,21 @@ fun ChatDetailScreen(
} }
} }
// Кнопка меню с выпадающим списком // Кнопка меню - открывает bottom sheet
Box { IconButton(
IconButton( onClick = {
onClick = { showMenu = true
// 🔥 НЕ закрываем клавиатуру при открытии меню },
showMenu = true modifier = Modifier
}, .size(48.dp)
modifier = Modifier .clip(CircleShape)
.size(48.dp) ) {
.clip(CircleShape) Icon(
) { Icons.Default.MoreVert,
Icon( contentDescription = "More",
Icons.Default.MoreVert, tint = headerIconColor,
contentDescription = "More", modifier = Modifier.size(26.dp)
tint = headerIconColor, )
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
)
}
} }
} }
} }
@@ -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,
modifier = Modifier properties = PopupProperties(
.graphicsLayer { focusable = true,
scaleX = scale dismissOnBackPress = true,
scaleY = scale dismissOnClickOutside = true
this.alpha = alpha )
transformOrigin = TransformOrigin(0.85f, 0f)
}
.width(200.dp)
.shadow(
elevation = 16.dp,
shape = RoundedCornerShape(14.dp),
ambientColor = Color.Black.copy(alpha = 0.12f),
spotColor = Color.Black.copy(alpha = 0.08f)
)
.clip(RoundedCornerShape(14.dp))
.background(menuBackgroundColor)
) { ) {
content() Column(
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
transformOrigin = TransformOrigin(1f, 0f) // Анимация от правого верхнего угла
}
.width(220.dp)
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(12.dp),
spotColor = PrimaryBlue.copy(alpha = 0.2f),
ambientColor = PrimaryBlue.copy(alpha = 0.1f)
)
.border(
width = 1.dp,
color = accentBorderColor,
shape = RoundedCornerShape(12.dp)
)
.clip(RoundedCornerShape(12.dp))
.background(menuBackgroundColor)
.padding(vertical = 8.dp)
) {
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 стиль
) )
} }
} }