feat: Implement custom modern popup menu with animations; enhance user interaction and design aesthetics
This commit is contained in:
@@ -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 стиль
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user