feat: Improve keyboard handling in selection mode for better user experience

This commit is contained in:
k1ngsterr1
2026-01-15 20:54:04 +05:00
parent 2b1b6eecef
commit 22a17e5fec
3 changed files with 278 additions and 249 deletions

View File

@@ -323,15 +323,22 @@ fun ChatDetailScreen(
var selectedMessages by remember { mutableStateOf<Set<String>>(emptySet()) }
val isSelectionMode = selectedMessages.isNotEmpty()
// 🔥 Закрываем клавиатуру и emoji picker когда открывается selection mode (action bar с Reply/Forward)
// Логирование изменений selection mode
LaunchedEffect(isSelectionMode, selectedMessages.size) {
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
android.util.Log.d("ChatDetailScreen", "📝 SELECTION MODE CHANGED")
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode: $isSelectionMode")
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size: ${selectedMessages.size}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
}
// 🔥 Backup: если клавиатура ещё открыта когда selection mode активировался
// (клавиатура уже должна быть закрыта в onLongClick, это только backup)
LaunchedEffect(isSelectionMode) {
if (isSelectionMode) {
// Используем нативный InputMethodManager для НАДЁЖНОГО закрытия клавиатуры
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
android.util.Log.d("ChatDetailScreen", "⚠️ Backup keyboard hide triggered")
// Backup закрытие клавиатуры (основное в onLongClick)
keyboardController?.hide()
focusManager.clearFocus()
showEmojiPicker = false
}
}
@@ -542,164 +549,140 @@ fun ChatDetailScreen(
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
// 🔥 SELECTION HEADER (появляется при выборе сообщений) - Telegram Style
AnimatedVisibility(
visible = isSelectionMode,
enter = fadeIn(animationSpec = tween(200)) + slideInVertically(
initialOffsetY = { -it },
animationSpec = tween(250, easing = TelegramEasing)
),
exit = fadeOut(animationSpec = tween(150)) + slideOutVertically(
targetOffsetY = { -it },
animationSpec = tween(200, easing = TelegramEasing)
)
) {
Box(modifier = Modifier
// 🔥 UNIFIED HEADER - один контейнер, контент меняется внутри
Box(
modifier = Modifier
.fillMaxWidth()
.background(if (isDarkTheme) Color(0xFF212121) else Color.White)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// Left: X (cancel) + Count - Telegram Style
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { selectedMessages = emptySet() }) {
Icon(
Icons.Default.Close,
contentDescription = "Cancel",
tint = if (isDarkTheme) Color.White else Color.Black,
modifier = Modifier.size(24.dp)
.background(if (isSelectionMode) {
if (isDarkTheme) Color(0xFF212121) else Color.White
} else headerBackground)
) {
// Контент хедера с Crossfade для плавной смены
Crossfade(
targetState = isSelectionMode,
animationSpec = tween(200),
label = "headerContent"
) { selectionMode ->
if (selectionMode) {
// SELECTION MODE CONTENT
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// Left: X (cancel) + Count
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { selectedMessages = emptySet() }) {
Icon(
Icons.Default.Close,
contentDescription = "Cancel",
tint = if (isDarkTheme) Color.White else Color.Black,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
"${selectedMessages.size}",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = if (isDarkTheme) Color.White else Color.Black
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
"${selectedMessages.size}",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = if (isDarkTheme) Color.White else Color.Black
)
}
// Right: Action buttons - Telegram Style
// Right: Action buttons
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Copy button
IconButton(
onClick = {
val textToCopy = messages
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
.sortedBy { it.timestamp }
.joinToString("\n\n") { msg ->
val time = SimpleDateFormat("HH:mm", Locale.getDefault())
.format(msg.timestamp)
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n${msg.text}"
}
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(textToCopy))
selectedMessages = emptySet()
}
) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy",
tint = if (isDarkTheme) Color.White else Color.Black,
modifier = Modifier.size(22.dp)
)
}
// Delete button
IconButton(
onClick = {
messages
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
.forEach { msg -> viewModel.deleteMessage(msg.id) }
selectedMessages = emptySet()
}
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
tint = if (isDarkTheme) Color.White else Color.Black,
modifier = Modifier.size(22.dp)
)
}
}
}
} else {
// NORMAL HEADER CONTENT
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Copy button
IconButton(
onClick = {
val textToCopy = messages
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
.sortedBy { it.timestamp }
.joinToString("\n\n") { msg ->
val time = SimpleDateFormat("HH:mm", Locale.getDefault())
.format(msg.timestamp)
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n${msg.text}"
}
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(textToCopy))
selectedMessages = emptySet()
// Back button with badge
Box {
IconButton(
onClick = hideKeyboardAndBack,
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Default.KeyboardArrowLeft,
contentDescription = "Back",
tint = headerIconColor,
modifier = Modifier.size(32.dp)
)
}
) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy",
tint = if (isDarkTheme) Color.White else Color.Black,
modifier = Modifier.size(22.dp)
)
}
// Delete button
IconButton(
onClick = {
// Удаляем выбранные сообщения
messages
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
.forEach { msg -> viewModel.deleteMessage(msg.id) }
selectedMessages = emptySet()
if (totalUnreadFromOthers > 0) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = (-4).dp, y = 6.dp)
.size(if (totalUnreadFromOthers > 9) 20.dp else 18.dp)
.clip(CircleShape)
.background(Color(0xFFFF3B30)),
contentAlignment = Alignment.Center
) {
Text(
text = if (totalUnreadFromOthers > 99) "99+"
else "$totalUnreadFromOthers",
color = Color.White,
fontSize = if (totalUnreadFromOthers > 9) 9.sp else 10.sp,
fontWeight = FontWeight.Bold,
maxLines = 1
)
}
}
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
tint = if (isDarkTheme) Color.White else Color.Black,
modifier = Modifier.size(22.dp)
)
}
}
}
// Bottom line
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme) Color.White.copy(alpha = 0.15f)
else Color.Black.copy(alpha = 0.1f)
)
)
}
}
// NORMAL HEADER (скрывается при выборе)
AnimatedVisibility(
visible = !isSelectionMode,
enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(150))
) {
// Telegram-style TopAppBar - solid background без blur
Box(modifier = Modifier.fillMaxWidth().background(headerBackground)) {
Row(
modifier =
Modifier.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 🔔 Кнопка назад с badge непрочитанных сообщений
Box {
IconButton(
onClick = hideKeyboardAndBack,
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Default.KeyboardArrowLeft,
contentDescription = "Back",
tint = headerIconColor,
modifier = Modifier.size(32.dp)
)
}
// Badge с количеством непрочитанных из других чатов
if (totalUnreadFromOthers > 0) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = (-4).dp, y = 6.dp)
.size(if (totalUnreadFromOthers > 9) 20.dp else 18.dp)
.clip(CircleShape)
.background(Color(0xFFFF3B30)), // Красный цвет как в iOS
contentAlignment = Alignment.Center
) {
Text(
text = if (totalUnreadFromOthers > 99) "99+"
else if (totalUnreadFromOthers > 9) "$totalUnreadFromOthers"
else "$totalUnreadFromOthers",
color = Color.White,
fontSize = if (totalUnreadFromOthers > 9) 9.sp else 10.sp,
fontWeight = FontWeight.Bold,
maxLines = 1
)
}
}
}
Spacer(modifier = Modifier.width(4.dp))
@@ -962,20 +945,21 @@ fun ChatDetailScreen(
}
}
}
// Нижняя линия для разделения
}
} // Закрытие Crossfade
// Bottom line для unified header
Box(
modifier =
Modifier.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme)
Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
)
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme) Color.White.copy(alpha = 0.15f)
else Color.Black.copy(alpha = 0.1f)
)
)
}
} // Закрытие AnimatedVisibility для normal header
} // Закрытие Box unified header
},
containerColor = backgroundColor, // Фон всего чата
// 🔥 Bottom bar - инпут с умным padding:
@@ -984,76 +968,38 @@ fun ChatDetailScreen(
bottomBar = {
// 🔥 Telegram-style: когда Box с эмодзи виден, НЕ используем imePadding
// isEmojiBoxVisible учитывает анимацию fade-out (alpha > 0.01)
val bottomModifier = if (coordinator.isEmojiBoxVisible) {
Modifier // Без imePadding - Box с эмодзи заменяет клавиатуру
} else {
// 🔥 В selection mode НЕ используем imePadding (клавиатура закрыта)
val useImePadding = !coordinator.isEmojiBoxVisible && !isSelectionMode
val bottomModifier = if (useImePadding) {
Modifier.imePadding() // С imePadding - клавиатура поднимает инпут
}
Column(modifier = bottomModifier) {
// 🔥 FLOATING INPUT BAR - плавает поверх сообщений, поднимается с клавиатурой
// Скрываем когда в режиме выбора
AnimatedVisibility(
visible = !isSelectionMode,
enter = fadeIn(tween(200)) + slideInVertically(initialOffsetY = { it }),
exit = fadeOut(tween(150)) + slideOutVertically(targetOffsetY = { it })
) {
Column {
// Input bar с встроенным reply preview (как в React Native)
MessageInputBar(
value = inputText,
onValueChange = {
viewModel.updateInputText(it)
// Отправляем индикатор печатания
if (it.isNotEmpty() && !isSavedMessages) {
viewModel.sendTypingIndicator()
}
},
onSend = {
// Скрываем кнопку scroll на время отправки
isSendingMessage = true
viewModel.sendMessage()
// Скроллим к новому сообщению
scope.launch {
delay(100)
listState.animateScrollToItem(0)
delay(300) // Ждём завершения анимации
isSendingMessage = false
}
},
isDarkTheme = isDarkTheme,
backgroundColor = backgroundColor, // Тот же цвет что и фон чата
textColor = textColor,
placeholderColor = secondaryTextColor,
secondaryTextColor = secondaryTextColor,
// Reply state
replyMessages = replyMessages,
isForwardMode = isForwardMode,
onCloseReply = { viewModel.clearReplyMessages() },
chatTitle = chatTitle,
isBlocked = isBlocked,
// Emoji picker state (поднят для KeyboardAvoidingView)
showEmojiPicker = showEmojiPicker,
onToggleEmojiPicker = { showEmojiPicker = it },
// Focus requester для автофокуса при reply
focusRequester = inputFocusRequester,
// Coordinator для плавных переходов
coordinator = coordinator,
// 🔥 Для отображения reply preview и скролла
displayReplyMessages = displayReplyMessages,
onReplyClick = scrollToMessage
)
}
} else {
Modifier // Без imePadding
}
// 🔥 SELECTION ACTION BAR - Reply/Forward (появляется при выборе сообщений)
// Плоский стиль как у инпута с border сверху
AnimatedVisibility(
visible = isSelectionMode,
enter = fadeIn(tween(200)) + slideInVertically(initialOffsetY = { it }),
exit = fadeOut(tween(150)) + slideOutVertically(targetOffsetY = { it })
) {
Column {
// Плоский контейнер как у инпута
// Логирование состояния
LaunchedEffect(isSelectionMode, useImePadding, coordinator.isEmojiBoxVisible, coordinator.keyboardHeight) {
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
android.util.Log.d("ChatDetailScreen", "🔄 BOTTOM BAR STATE CHANGED")
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode: $isSelectionMode")
android.util.Log.d("ChatDetailScreen", " 📊 useImePadding: $useImePadding")
android.util.Log.d("ChatDetailScreen", " 📊 isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}")
android.util.Log.d("ChatDetailScreen", " 📊 keyboardHeight: ${coordinator.keyboardHeight}")
android.util.Log.d("ChatDetailScreen", " 📊 emojiHeight: ${coordinator.emojiHeight}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
}
Column(modifier = bottomModifier) {
// 🔥 UNIFIED BOTTOM BAR - один контейнер, контент меняется внутри
Crossfade(
targetState = isSelectionMode,
animationSpec = tween(200),
label = "bottomBarContent"
) { selectionMode ->
android.util.Log.d("ChatDetailScreen", "🎬 Crossfade to selectionMode=$selectionMode")
if (selectionMode) {
// SELECTION ACTION BAR - Reply/Forward
// 🔥 Высота должна совпадать с MessageInputBar (~56dp content + nav bar)
Column(
modifier = Modifier
.fillMaxWidth()
@@ -1067,12 +1013,12 @@ fun ChatDetailScreen(
.background(if (isDarkTheme) Color.White.copy(alpha = 0.15f) else Color.Black.copy(alpha = 0.1f))
)
// Кнопки Reply и Forward
// Кнопки Reply и Forward - такие же отступы как у input row
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
.padding(bottom = 12.dp)
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(bottom = 16.dp)
.navigationBarsPadding(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
@@ -1081,6 +1027,7 @@ fun ChatDetailScreen(
Box(
modifier = Modifier
.weight(1f)
.height(40.dp)
.clip(RoundedCornerShape(12.dp))
.background(PrimaryBlue.copy(alpha = 0.1f))
.clickable {
@@ -1089,8 +1036,7 @@ fun ChatDetailScreen(
.sortedBy { it.timestamp }
viewModel.setReplyMessages(selectedMsgs)
selectedMessages = emptySet()
}
.padding(vertical = 16.dp),
},
contentAlignment = Alignment.Center
) {
Row(
@@ -1117,6 +1063,7 @@ fun ChatDetailScreen(
Box(
modifier = Modifier
.weight(1f)
.height(40.dp)
.clip(RoundedCornerShape(12.dp))
.background(PrimaryBlue.copy(alpha = 0.1f))
.clickable {
@@ -1125,8 +1072,7 @@ fun ChatDetailScreen(
.sortedBy { it.timestamp }
viewModel.setForwardMessages(selectedMsgs)
selectedMessages = emptySet()
}
.padding(vertical = 16.dp),
},
contentAlignment = Alignment.Center
) {
Row(
@@ -1150,6 +1096,45 @@ fun ChatDetailScreen(
}
}
}
} else {
// INPUT BAR
Column {
MessageInputBar(
value = inputText,
onValueChange = {
viewModel.updateInputText(it)
if (it.isNotEmpty() && !isSavedMessages) {
viewModel.sendTypingIndicator()
}
},
onSend = {
isSendingMessage = true
viewModel.sendMessage()
scope.launch {
delay(100)
listState.animateScrollToItem(0)
delay(300)
isSendingMessage = false
}
},
isDarkTheme = isDarkTheme,
backgroundColor = backgroundColor,
textColor = textColor,
placeholderColor = secondaryTextColor,
secondaryTextColor = secondaryTextColor,
replyMessages = replyMessages,
isForwardMode = isForwardMode,
onCloseReply = { viewModel.clearReplyMessages() },
chatTitle = chatTitle,
isBlocked = isBlocked,
showEmojiPicker = showEmojiPicker,
onToggleEmojiPicker = { showEmojiPicker = it },
focusRequester = inputFocusRequester,
coordinator = coordinator,
displayReplyMessages = displayReplyMessages,
onReplyClick = scrollToMessage
)
}
}
}
} // Закрытие Column с imePadding
@@ -1288,12 +1273,29 @@ fun ChatDetailScreen(
isSelected = selectedMessages.contains(selectionKey),
isHighlighted = highlightedMessageId == message.id,
onLongClick = {
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
android.util.Log.d("ChatDetailScreen", "👆 LONG CLICK on message")
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode BEFORE: $isSelectionMode")
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size BEFORE: ${selectedMessages.size}")
// 🔥 СНАЧАЛА закрываем клавиатуру МГНОВЕННО (до изменения state)
if (!isSelectionMode) {
android.util.Log.d("ChatDetailScreen", " ⌨️ Closing keyboard...")
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
focusManager.clearFocus()
showEmojiPicker = false
android.util.Log.d("ChatDetailScreen", " ✅ Keyboard closed")
}
// Toggle selection on long press
selectedMessages = if (selectedMessages.contains(selectionKey)) {
selectedMessages - selectionKey
} else {
selectedMessages + selectionKey
}
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size AFTER: ${selectedMessages.size}")
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
},
onClick = {
// If in selection mode, toggle selection
@@ -2332,11 +2334,14 @@ private fun MessageInputBar(
.background(if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f))
)
// BLOCKED CHAT FOOTER - плоский стиль
// 🔥 Высота должна совпадать с MessageInputBar и Selection Action Bar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
.padding(bottom = 16.dp),
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(bottom = 16.dp)
.navigationBarsPadding(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
@@ -2453,11 +2458,14 @@ private fun MessageInputBar(
}
// INPUT ROW - Paperclip → TextField → Emoji → Send/Mic (ПЛОСКИЙ ДИЗАЙН)
// 🔥 Высота должна совпадать с Selection Action Bar (padding + nav bar)
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 48.dp)
.padding(horizontal = 12.dp, vertical = 8.dp),
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(bottom = 16.dp) // 🔥 Такой же отступ как у Selection Action Bar
.navigationBarsPadding(), // 🔥 Такой же navigationBarsPadding
verticalAlignment = Alignment.Bottom
) {
// PAPERCLIP BUTTON (слева)

View File

@@ -768,8 +768,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* 🔥 Очистить reply/forward
*/
fun clearReplyMessages() {
_replyMessages.value = emptyList()
_isForwardMode.value = false
viewModelScope.launch {
delay(350) // Задержка после закрытия панели (анимация fadeOut + shrinkVertically)
_replyMessages.value = emptyList()
_isForwardMode.value = false
}
}
/**

View File

@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
* ```
*/
object KeyboardHeightProvider {
private const val TAG = "KeyboardHeight"
private const val PREFS_NAME = "emoji_keyboard_prefs"
private const val KEY_KEYBOARD_HEIGHT = "kbd_height"
private const val DEFAULT_HEIGHT_DP = 280 // Telegram uses 200, we use 280
@@ -35,7 +36,11 @@ object KeyboardHeightProvider {
val savedPx = prefs.getInt(KEY_KEYBOARD_HEIGHT, defaultPx)
val isDefault = savedPx == defaultPx
android.util.Log.d("KeyboardHeight", "📖 getSavedKeyboardHeight: ${savedPx}px (${pxToDp(context, savedPx)}dp) ${if (isDefault) "[DEFAULT]" else "[SAVED]"}")
android.util.Log.d(TAG, "═══════════════════════════════════════")
android.util.Log.d(TAG, "📖 getSavedKeyboardHeight()")
android.util.Log.d(TAG, " 📏 Height: ${savedPx}px (${pxToDp(context, savedPx)}dp)")
android.util.Log.d(TAG, " 📦 Source: ${if (isDefault) "DEFAULT" else "SAVED"}")
android.util.Log.d(TAG, "═══════════════════════════════════════")
return savedPx
}
@@ -43,6 +48,9 @@ object KeyboardHeightProvider {
* Сохранить высоту клавиатуры
*/
fun saveKeyboardHeight(context: Context, heightPx: Int) {
android.util.Log.d(TAG, "═══════════════════════════════════════")
android.util.Log.d(TAG, "💾 saveKeyboardHeight($heightPx px)")
if (heightPx > 0) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val oldHeight = prefs.getInt(KEY_KEYBOARD_HEIGHT, -1)
@@ -50,21 +58,29 @@ object KeyboardHeightProvider {
prefs.edit().putInt(KEY_KEYBOARD_HEIGHT, heightPx).apply()
android.util.Log.d(TAG, " 📏 New: ${heightPx}px (${pxToDp(context, heightPx)}dp)")
android.util.Log.d(TAG, " 📏 Old: ${oldHeight}px (${pxToDp(context, oldHeight)}dp)")
android.util.Log.d(TAG, " 🔄 Changed: $changed")
if (changed) {
android.util.Log.d("KeyboardHeight", "💾 SAVED keyboard height: ${heightPx}px (${pxToDp(context, heightPx)}dp) [WAS: ${oldHeight}px]")
android.util.Log.d(TAG, " SAVED successfully!")
} else {
android.util.Log.v("KeyboardHeight", "💾 Same keyboard height: ${heightPx}px (no change)")
android.util.Log.d(TAG, " ⏭️ Same height, no change")
}
} else {
android.util.Log.w("KeyboardHeight", "⚠️ Attempted to save invalid height: ${heightPx}px")
android.util.Log.w(TAG, " ⚠️ INVALID height: ${heightPx}px - NOT saved!")
}
android.util.Log.d(TAG, "═══════════════════════════════════════")
}
/**
* Получить длительность анимации клавиатуры
* Telegram использует: AdjustPanLayoutHelper.keyboardDuration (250ms обычно)
*/
fun getKeyboardAnimationDuration(): Long = 250L
fun getKeyboardAnimationDuration(): Long {
android.util.Log.d(TAG, "⏱️ getKeyboardAnimationDuration() = 250ms")
return 250L
}
// Helper functions
private fun dpToPx(context: Context, dp: Int): Int {
@@ -91,7 +107,9 @@ fun rememberSavedKeyboardHeight(): Dp {
val density = context.resources.displayMetrics.density
val heightDp = (heightPx / density).dp
android.util.Log.d("KeyboardHeight", "🎯 rememberSavedKeyboardHeight: ${heightDp} (${heightPx}px, density=${density})")
android.util.Log.d("KeyboardHeight", "🎯 rememberSavedKeyboardHeight()")
android.util.Log.d("KeyboardHeight", " 📏 Result: $heightDp (${heightPx}px)")
android.util.Log.d("KeyboardHeight", " 📱 Density: $density")
return heightDp
}