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:
1
app/src/main/assets/lottie/speech.json
Normal file
1
app/src/main/assets/lottie/speech.json
Normal file
File diff suppressed because one or more lines are too long
@@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.rosetta.messenger.R
|
||||
import com.rosetta.messenger.data.Message
|
||||
import com.rosetta.messenger.network.DeliveryStatus
|
||||
import com.rosetta.messenger.network.SearchUser
|
||||
@@ -65,6 +66,7 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import com.airbnb.lottie.compose.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -202,6 +204,15 @@ fun ChatDetailScreen(
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
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
|
||||
var wasManualScroll by remember { mutableStateOf(false) }
|
||||
@@ -675,9 +686,10 @@ fun ChatDetailScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.imePadding() // KeyboardAvoidingView equivalent
|
||||
) {
|
||||
// Список сообщений - занимает весь экран
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(bottom = 70.dp)) {
|
||||
if (messages.isEmpty()) {
|
||||
// Пустое состояние
|
||||
Column(
|
||||
@@ -685,13 +697,25 @@ fun ChatDetailScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
if (isSavedMessages) Icons.Default.Bookmark
|
||||
else Icons.Default.Chat,
|
||||
if (isSavedMessages) {
|
||||
Icon(
|
||||
Icons.Default.Bookmark,
|
||||
contentDescription = null,
|
||||
tint = secondaryTextColor.copy(alpha = 0.5f),
|
||||
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))
|
||||
Text(
|
||||
text =
|
||||
@@ -737,13 +761,13 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
),
|
||||
// padding для контента списка - добавляем снизу место для инпута
|
||||
// padding для контента списка - фиксированный для инпута
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = 8.dp,
|
||||
bottom = 100.dp // Место для floating input (с запасом)
|
||||
bottom = 16.dp // Небольшой отступ снизу
|
||||
),
|
||||
reverseLayout = true
|
||||
) {
|
||||
@@ -897,7 +921,7 @@ fun ChatDetailScreen(
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = inputBackgroundColor,
|
||||
backgroundColor = backgroundColor, // Тот же цвет что и фон чата
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor,
|
||||
secondaryTextColor = secondaryTextColor,
|
||||
@@ -1538,6 +1562,13 @@ private fun MessageInputBar(
|
||||
|
||||
// Состояние отправки
|
||||
val canSend = remember(value) { value.isNotBlank() }
|
||||
|
||||
// 🔥 Закрываем эмодзи панель когда клавиатура открывается
|
||||
LaunchedEffect(isKeyboardVisible) {
|
||||
if (isKeyboardVisible && showEmojiPicker) {
|
||||
showEmojiPicker = false
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для гарантированного закрытия клавиатуры через InputMethodManager
|
||||
fun hideKeyboardCompletely() {
|
||||
@@ -1612,15 +1643,30 @@ private fun MessageInputBar(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 🔥 TELEGRAM STYLE: простой фон, все кнопки внутри
|
||||
// 🔥 TELEGRAM STYLE: фон как у чата, верхний border
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.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
|
||||
AnimatedVisibility(
|
||||
visible = hasReply,
|
||||
@@ -1630,7 +1676,7 @@ private fun MessageInputBar(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFFFFFFF))
|
||||
.background(backgroundColor) // Тот же цвет что и фон чата
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -1679,38 +1725,47 @@ private fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
|
||||
// INPUT ROW - как в Telegram
|
||||
// INPUT ROW - Paperclip → TextField → Emoji → Send/Mic (ПЛОСКИЙ ДИЗАЙН)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
// EMOJI BUTTON (слева)
|
||||
// PAPERCLIP BUTTON (слева)
|
||||
IconButton(
|
||||
onClick = { toggleEmojiPicker() },
|
||||
modifier = Modifier.size(48.dp)
|
||||
onClick = { /* TODO: Attach file/image */ },
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
if (showEmojiPicker) Icons.Default.Keyboard
|
||||
else Icons.Default.SentimentSatisfiedAlt,
|
||||
contentDescription = "Emoji",
|
||||
Icons.Default.AttachFile,
|
||||
contentDescription = "Attach",
|
||||
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(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = 36.dp)
|
||||
.heightIn(min = 40.dp)
|
||||
.background(
|
||||
color = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE5E5EA),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
color = backgroundColor // Тот же цвет что и фон чата
|
||||
)
|
||||
.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
|
||||
) {
|
||||
AppleEmojiTextField(
|
||||
@@ -1718,60 +1773,69 @@ private fun MessageInputBar(
|
||||
onValueChange = { newValue -> onValueChange(newValue) },
|
||||
textColor = textColor,
|
||||
textSize = 16f,
|
||||
hint = "Message",
|
||||
hint = "Type message...",
|
||||
hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
// ATTACH / MIC / SEND BUTTON (справа)
|
||||
if (canSend) {
|
||||
// SEND BUTTON
|
||||
// EMOJI BUTTON (между input и send)
|
||||
IconButton(
|
||||
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(
|
||||
onClick = { handleSend() },
|
||||
modifier = Modifier.size(48.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)
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Mic,
|
||||
contentDescription = "Voice",
|
||||
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
||||
modifier = Modifier.size(26.dp)
|
||||
imageVector = TelegramSendIcon,
|
||||
contentDescription = "Send",
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Закрытие внутренней Column с padding
|
||||
} // Закрытие внешней Column с border
|
||||
} // End of else (not blocked)
|
||||
|
||||
// 🔥 APPLE EMOJI PICKER - плавная анимация высоты
|
||||
// 🔥 APPLE EMOJI PICKER - БЕЗ анимации когда клавиатура открывается
|
||||
if (!isBlocked) {
|
||||
// Анимируем высоту панели
|
||||
// Высота панели: 0 если клавиатура видна или эмодзи закрыты, иначе emojiPanelHeight
|
||||
// НЕ анимируем когда клавиатура открыта (чтобы не было прыжка)
|
||||
val targetHeight = if (isKeyboardVisible || !showEmojiPicker) 0.dp else emojiPanelHeight
|
||||
|
||||
// Анимируем только когда клавиатура закрыта
|
||||
val animatedHeight by animateDpAsState(
|
||||
targetValue = if (showEmojiPicker) emojiPanelHeight else 0.dp,
|
||||
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
|
||||
targetValue = targetHeight,
|
||||
animationSpec = if (isKeyboardVisible) {
|
||||
// Мгновенно когда клавиатура открывается
|
||||
snap()
|
||||
} else {
|
||||
// Плавно когда открываем/закрываем эмодзи без клавиатуры
|
||||
tween(durationMillis = 200, easing = FastOutSlowInEasing)
|
||||
},
|
||||
label = "EmojiPanelHeight"
|
||||
)
|
||||
|
||||
@@ -1781,7 +1845,7 @@ private fun MessageInputBar(
|
||||
.height(animatedHeight)
|
||||
.clipToBounds()
|
||||
) {
|
||||
if (showEmojiPicker) {
|
||||
if (showEmojiPicker && !isKeyboardVisible) {
|
||||
AppleEmojiPickerPanel(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = { emoji ->
|
||||
|
||||
1
app/src/main/res/raw/speech.json
Normal file
1
app/src/main/res/raw/speech.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user