feat: Enhance ChatDetailScreen with dynamic keyboard handling and new animations

- Implemented dynamic bottom padding for the message list based on keyboard visibility.
- Added Lottie animation for the speech icon when no messages are present.
- Adjusted input bar design to match chat background color and improved spacing.
- Integrated emoji picker behavior to close when the keyboard opens.
- Updated message input row with attach file functionality and refined button sizes.
- Introduced a new Lottie animation resource for the speech icon.
This commit is contained in:
k1ngsterr1
2026-01-13 02:46:16 +05:00
parent 8720d02701
commit b26c25629a
3 changed files with 131 additions and 65 deletions

File diff suppressed because one or more lines are too long

View File

@@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.rosetta.messenger.R
import com.rosetta.messenger.data.Message import com.rosetta.messenger.data.Message
import com.rosetta.messenger.network.DeliveryStatus import com.rosetta.messenger.network.DeliveryStatus
import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.network.SearchUser
@@ -65,6 +66,7 @@ import android.view.inputmethod.InputMethodManager
import android.content.Context import android.content.Context
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import com.airbnb.lottie.compose.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -202,6 +204,15 @@ fun ChatDetailScreen(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val density = LocalDensity.current
// 🔥 Отслеживаем высоту клавиатуры для поднятия контента
val imeInsets = WindowInsets.ime
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
val isKeyboardVisible = imeHeight > 0.dp
// Динамический bottom padding для списка: инпут (~70dp) + клавиатура
val listBottomPadding = if (isKeyboardVisible) 70.dp + imeHeight else 100.dp
// Telegram-style scroll tracking // Telegram-style scroll tracking
var wasManualScroll by remember { mutableStateOf(false) } var wasManualScroll by remember { mutableStateOf(false) }
@@ -675,9 +686,10 @@ fun ChatDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.imePadding() // KeyboardAvoidingView equivalent
) { ) {
// Список сообщений - занимает весь экран // Список сообщений - занимает весь экран
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize().padding(bottom = 70.dp)) {
if (messages.isEmpty()) { if (messages.isEmpty()) {
// Пустое состояние // Пустое состояние
Column( Column(
@@ -685,13 +697,25 @@ fun ChatDetailScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Icon( if (isSavedMessages) {
if (isSavedMessages) Icons.Default.Bookmark Icon(
else Icons.Default.Chat, Icons.Default.Bookmark,
contentDescription = null, contentDescription = null,
tint = secondaryTextColor.copy(alpha = 0.5f), tint = secondaryTextColor.copy(alpha = 0.5f),
modifier = Modifier.size(64.dp) modifier = Modifier.size(64.dp)
) )
} else {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.speech))
val progress by animateLottieCompositionAsState(
composition = composition,
iterations = LottieConstants.IterateForever
)
LottieAnimation(
composition = composition,
progress = { progress },
modifier = Modifier.size(120.dp)
)
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = text =
@@ -737,13 +761,13 @@ fun ChatDetailScreen(
} }
} }
), ),
// padding для контента списка - добавляем снизу место для инпута // padding для контента списка - фиксированный для инпута
contentPadding = contentPadding =
PaddingValues( PaddingValues(
start = 8.dp, start = 8.dp,
end = 8.dp, end = 8.dp,
top = 8.dp, top = 8.dp,
bottom = 100.dp // Место для floating input (с запасом) bottom = 16.dp // Небольшой отступ снизу
), ),
reverseLayout = true reverseLayout = true
) { ) {
@@ -897,7 +921,7 @@ fun ChatDetailScreen(
} }
}, },
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
backgroundColor = inputBackgroundColor, backgroundColor = backgroundColor, // Тот же цвет что и фон чата
textColor = textColor, textColor = textColor,
placeholderColor = secondaryTextColor, placeholderColor = secondaryTextColor,
secondaryTextColor = secondaryTextColor, secondaryTextColor = secondaryTextColor,
@@ -1539,6 +1563,13 @@ private fun MessageInputBar(
// Состояние отправки // Состояние отправки
val canSend = remember(value) { value.isNotBlank() } val canSend = remember(value) { value.isNotBlank() }
// 🔥 Закрываем эмодзи панель когда клавиатура открывается
LaunchedEffect(isKeyboardVisible) {
if (isKeyboardVisible && showEmojiPicker) {
showEmojiPicker = false
}
}
// Функция для гарантированного закрытия клавиатуры через InputMethodManager // Функция для гарантированного закрытия клавиатуры через InputMethodManager
fun hideKeyboardCompletely() { fun hideKeyboardCompletely() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
@@ -1612,15 +1643,30 @@ private fun MessageInputBar(
) )
} }
} else { } else {
// 🔥 TELEGRAM STYLE: простой фон, все кнопки внутри // 🔥 TELEGRAM STYLE: фон как у чата, верхний border
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
)
.padding(bottom = if (isKeyboardVisible) 0.dp else 16.dp)
) { ) {
// Верхний border (как в архиве)
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme) Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
)
)
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = backgroundColor // Тот же цвет что и фон чата
)
.padding(bottom = if (isKeyboardVisible) 0.dp else 16.dp)
) {
// REPLY PANEL // REPLY PANEL
AnimatedVisibility( AnimatedVisibility(
visible = hasReply, visible = hasReply,
@@ -1630,7 +1676,7 @@ private fun MessageInputBar(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFFFFFFF)) .background(backgroundColor) // Тот же цвет что и фон чата
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -1679,38 +1725,47 @@ private fun MessageInputBar(
} }
} }
// INPUT ROW - как в Telegram // INPUT ROW - Paperclip → TextField → Emoji → Send/Mic (ПЛОСКИЙ ДИЗАЙН)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 48.dp) .heightIn(min = 48.dp)
.padding(horizontal = 8.dp, vertical = 8.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
// EMOJI BUTTON (слева) // PAPERCLIP BUTTON (слева)
IconButton( IconButton(
onClick = { toggleEmojiPicker() }, onClick = { /* TODO: Attach file/image */ },
modifier = Modifier.size(48.dp) modifier = Modifier.size(40.dp)
) { ) {
Icon( Icon(
if (showEmojiPicker) Icons.Default.Keyboard Icons.Default.AttachFile,
else Icons.Default.SentimentSatisfiedAlt, contentDescription = "Attach",
contentDescription = "Emoji",
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93), tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.size(26.dp) modifier = Modifier.size(24.dp)
) )
} }
// TEXT INPUT Spacer(modifier = Modifier.width(4.dp))
// TEXT INPUT - ПЛОСКИЙ (тот же цвет что и фон чата)
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.heightIn(min = 36.dp) .heightIn(min = 40.dp)
.background( .background(
color = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE5E5EA), color = backgroundColor // Тот же цвет что и фон чата
shape = RoundedCornerShape(20.dp)
) )
.padding(horizontal = 12.dp, vertical = 10.dp), .clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
// При клике на инпут - закрываем эмодзи панель, клавиатура откроется автоматически
if (showEmojiPicker) {
showEmojiPicker = false
}
}
.padding(horizontal = 12.dp, vertical = 8.dp),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
AppleEmojiTextField( AppleEmojiTextField(
@@ -1718,60 +1773,69 @@ private fun MessageInputBar(
onValueChange = { newValue -> onValueChange(newValue) }, onValueChange = { newValue -> onValueChange(newValue) },
textColor = textColor, textColor = textColor,
textSize = 16f, textSize = 16f,
hint = "Message", hint = "Type message...",
hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93), hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(6.dp))
// ATTACH / MIC / SEND BUTTON (справа) // EMOJI BUTTON (между input и send)
if (canSend) { IconButton(
// SEND BUTTON onClick = { toggleEmojiPicker() },
modifier = Modifier.size(40.dp)
) {
Icon(
if (showEmojiPicker) Icons.Default.Keyboard
else Icons.Default.SentimentSatisfiedAlt,
contentDescription = "Emoji",
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(2.dp))
// SEND BUTTON (всегда справа) - с анимацией
AnimatedVisibility(
visible = canSend,
enter = scaleIn(tween(150)) + fadeIn(tween(150)),
exit = scaleOut(tween(100)) + fadeOut(tween(100))
) {
IconButton( IconButton(
onClick = { handleSend() }, onClick = { handleSend() },
modifier = Modifier.size(48.dp) modifier = Modifier.size(40.dp)
) {
Box(
modifier = Modifier
.size(34.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = TelegramSendIcon,
contentDescription = "Send",
tint = Color.White,
modifier = Modifier.size(18.dp)
)
}
}
} else {
// MIC BUTTON
IconButton(
onClick = { /* TODO: Voice recording */ },
modifier = Modifier.size(48.dp)
) { ) {
Icon( Icon(
Icons.Default.Mic, imageVector = TelegramSendIcon,
contentDescription = "Voice", contentDescription = "Send",
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93), tint = PrimaryBlue,
modifier = Modifier.size(26.dp) modifier = Modifier.size(24.dp)
) )
} }
} }
} }
} } // Закрытие внутренней Column с padding
} // Закрытие внешней Column с border
} // End of else (not blocked) } // End of else (not blocked)
// 🔥 APPLE EMOJI PICKER - плавная анимация высоты // 🔥 APPLE EMOJI PICKER - БЕЗ анимации когда клавиатура открывается
if (!isBlocked) { if (!isBlocked) {
// Анимируем высоту панели // Высота панели: 0 если клавиатура видна или эмодзи закрыты, иначе emojiPanelHeight
// НЕ анимируем когда клавиатура открыта (чтобы не было прыжка)
val targetHeight = if (isKeyboardVisible || !showEmojiPicker) 0.dp else emojiPanelHeight
// Анимируем только когда клавиатура закрыта
val animatedHeight by animateDpAsState( val animatedHeight by animateDpAsState(
targetValue = if (showEmojiPicker) emojiPanelHeight else 0.dp, targetValue = targetHeight,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), animationSpec = if (isKeyboardVisible) {
// Мгновенно когда клавиатура открывается
snap()
} else {
// Плавно когда открываем/закрываем эмодзи без клавиатуры
tween(durationMillis = 200, easing = FastOutSlowInEasing)
},
label = "EmojiPanelHeight" label = "EmojiPanelHeight"
) )
@@ -1781,7 +1845,7 @@ private fun MessageInputBar(
.height(animatedHeight) .height(animatedHeight)
.clipToBounds() .clipToBounds()
) { ) {
if (showEmojiPicker) { if (showEmojiPicker && !isKeyboardVisible) {
AppleEmojiPickerPanel( AppleEmojiPickerPanel(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji -> onEmojiSelected = { emoji ->

File diff suppressed because one or more lines are too long