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.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
|
||||||
) {
|
) {
|
||||||
|
if (isSavedMessages) {
|
||||||
Icon(
|
Icon(
|
||||||
if (isSavedMessages) Icons.Default.Bookmark
|
Icons.Default.Bookmark,
|
||||||
else Icons.Default.Chat,
|
|
||||||
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,12 +1643,27 @@ private fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 🔥 TELEGRAM STYLE: простой фон, все кнопки внутри
|
// 🔥 TELEGRAM STYLE: фон как у чата, верхний border
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// Верхний 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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
color = backgroundColor // Тот же цвет что и фон чата
|
||||||
)
|
)
|
||||||
.padding(bottom = if (isKeyboardVisible) 0.dp else 16.dp)
|
.padding(bottom = if (isKeyboardVisible) 0.dp else 16.dp)
|
||||||
) {
|
) {
|
||||||
@@ -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(
|
Icon(
|
||||||
imageVector = TelegramSendIcon,
|
imageVector = TelegramSendIcon,
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
tint = Color.White,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// MIC BUTTON
|
|
||||||
IconButton(
|
|
||||||
onClick = { /* TODO: Voice recording */ },
|
|
||||||
modifier = Modifier.size(48.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Mic,
|
|
||||||
contentDescription = "Voice",
|
|
||||||
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
|
||||||
modifier = Modifier.size(26.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 ->
|
||||||
|
|||||||
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