diff --git a/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt b/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt index 96d2d5f..5989f6c 100644 --- a/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt +++ b/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt @@ -1,31 +1,33 @@ package app.rosette.android.ui.keyboard -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.dp import android.util.Log -import kotlinx.coroutines.launch /** - * Анимированный контейнер для emoji панели с Telegram-style переходами. + * 🚀 МЕТОД: Animated Height + Content Offset (Telegram-style) * - * Паттерны Telegram: - * - Keyboard → Emoji: панель выезжает снизу за 250ms (FastOutSlowIn) - * - Emoji → Keyboard: панель уезжает вниз за 200ms (FastOutLinearIn) - * - TranslationY + Alpha для плавности - * - Немедленное резервирование места + * Ключевые принципы: + * 1. ВЫСОТА контейнера анимируется от 0dp до emojiHeight + * 2. Контент внутри clipToBounds - обрезается по границам + * 3. НЕТ предварительного резервирования места когда скрыт + * 4. graphicsLayer для GPU-ускорения + * + * Паттерн: + * - Показать: height анимируется от 0dp до emojiHeight (контент появляется снизу) + * - Скрыть: height анимируется от emojiHeight до 0dp (контент уходит вниз) + * - Длительность: 250ms с FastOutSlowInEasing */ @Composable fun AnimatedKeyboardTransition( @@ -35,97 +37,65 @@ fun AnimatedKeyboardTransition( ) { val tag = "AnimatedTransition" - // Animatable для плавной анимации offset - val scope = rememberCoroutineScope() - val offsetY = remember { Animatable(0f) } - val alpha = remember { Animatable(1f) } - - // Отслеживаем изменения showEmojiPicker - LaunchedEffect(showEmojiPicker) { - Log.d(tag, "════════════════════════════════════════════════════════") - Log.d(tag, "🎬 Animation triggered: showEmojiPicker=$showEmojiPicker") - Log.d(tag, "📊 Current state: offsetY=${offsetY.value}dp, alpha=${alpha.value}, emojiHeight=${coordinator.emojiHeight.value}dp") - - if (showEmojiPicker) { - // ============ ПОКАЗАТЬ EMOJI ============ - - val height = coordinator.emojiHeight.value - Log.d(tag, "📤 Animating emoji IN from ${height}dp to 0dp") - Log.d(tag, "⏱️ Duration: ${KeyboardTransitionCoordinator.TRANSITION_DURATION}ms, Easing: FastOutSlowIn") - - // Начальная позиция: внизу за экраном - Log.d(tag, "🎯 Setting initial offset to ${height}dp (off-screen)") - offsetY.snapTo(height) - scope.launch { - offsetY.animateTo( - targetValue = 0f, - animationSpec = tween( - durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(), - easing = FastOutSlowInEasing - ) - ) - Log.d(tag, "✅ Emoji slide animation completed") - } - - // Fade in (немного быстрее) - Log.d(tag, "✨ Starting fade in animation (200ms)") - scope.launch { - alpha.animateTo( - targetValue = 1f, - animationSpec = tween(durationMillis = 200) - ) - Log.d(tag, "✅ Fade in completed, alpha=${alpha.value}") - } - - } else if (offsetY.value < coordinator.emojiHeight.value * 0.9f) { - // ============ СКРЫТЬ EMOJI ============ - Log.d(tag, "📥 Hiding emoji panel (offsetY=${offsetY.value}dp < threshold=${coordinator.emojiHeight.value * 0.9f}dp)") - - val height = coordinator.emojiHeight.value - Log.d(tag, "🎯 Animating emoji OUT from ${offsetY.value}dp to ${height}dp") - Log.d(tag, "⏱️ Duration: 200ms, Easing: FastOutLinearIn") - scope.launch { - offsetY.animateTo( - targetValue = height, - animationSpec = tween( - durationMillis = 200, - easing = FastOutLinearInEasing - ) - ) - Log.d(tag, "✅ Emoji hide animation completed") - } - - // Fade out (быстрее) - Log.d(tag, "🌑 Starting fade out animation (150ms)") - scope.launch { - alpha.animateTo( - targetValue = 0f, - animationSpec = tween(durationMillis = 150) - ) - Log.d(tag, "✅ Fade out completed, alpha=${alpha.value}") - } - } + // 🎯 Целевая высота: emojiHeight = видно, 0dp = скрыто + val targetHeight = if (showEmojiPicker) { + coordinator.emojiHeight + } else { + 0.dp } - // 🔥 Рендерим контент ТОЛЬКО если showEmojiPicker = true - // Не проверяем offsetY, чтобы избежать показа emoji при открытии клавиатуры - if (showEmojiPicker) { - Log.d(tag, "✅ Rendering emoji content (showEmojiPicker=true)") + // 🎬 Декларативная анимация высоты + val animatedHeight by animateDpAsState( + targetValue = targetHeight, + animationSpec = tween( + durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(), + easing = FastOutSlowInEasing + ), + label = "emojiPanelHeight" + ) + + Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, targetH=$targetHeight, currentH=$animatedHeight, emojiH=${coordinator.emojiHeight}") + + // 🔥 Рендерим только если есть высота (показан или анимируется) + if (animatedHeight > 0.dp) { Box( modifier = Modifier .fillMaxWidth() - .height(coordinator.emojiHeight) - .offset(y = offsetY.value.dp) - .alpha(alpha.value) + .height(animatedHeight) + .clipToBounds() // 🔥 Обрезаем контент по границам контейнера + .graphicsLayer { + // GPU-ускорение для плавности + clip = true + } ) { - content() + // Внутренний контейнер с полной высотой emoji + // Он обрезается clipToBounds родителя + Box( + modifier = Modifier + .fillMaxWidth() + .height(coordinator.emojiHeight) + // 🔥 Позиционируем снизу: когда animatedHeight < emojiHeight, + // верхняя часть обрезается, создавая эффект "выезда снизу" + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints.copy( + minHeight = 0, + maxHeight = coordinator.emojiHeight.roundToPx() + )) + layout(placeable.width, placeable.height) { + // Смещаем вниз так, чтобы нижняя часть была видна + val offsetY = placeable.height - animatedHeight.roundToPx() + placeable.place(0, -offsetY) + } + } + ) { + content() + } } } } /** - * Упрощенная версия без fade анимации. - * Только slide (как в оригинальном Telegram). + * Алиас для обратной совместимости */ @Composable fun SimpleAnimatedKeyboardTransition( @@ -133,39 +103,9 @@ fun SimpleAnimatedKeyboardTransition( showEmojiPicker: Boolean, content: @Composable () -> Unit ) { - val offsetY = remember { Animatable(0f) } - - LaunchedEffect(showEmojiPicker) { - if (showEmojiPicker) { - // Показать: снизу вверх - offsetY.snapTo(coordinator.emojiHeight.value) - offsetY.animateTo( - targetValue = 0f, - animationSpec = tween( - durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(), - easing = FastOutSlowInEasing - ) - ) - } else { - // Скрыть: сверху вниз - offsetY.animateTo( - targetValue = coordinator.emojiHeight.value, - animationSpec = tween( - durationMillis = 200, - easing = FastOutLinearInEasing - ) - ) - } - } - - if (showEmojiPicker || offsetY.value < coordinator.emojiHeight.value * 0.95f) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(coordinator.emojiHeight) - .offset(y = offsetY.value.dp) - ) { - content() - } - } -} + AnimatedKeyboardTransition( + coordinator = coordinator, + showEmojiPicker = showEmojiPicker, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt index 88c49d8..dfd14be 100644 --- a/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt +++ b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt @@ -72,7 +72,9 @@ class KeyboardTransitionCoordinator { /** * Переход от системной клавиатуры к emoji панели. - * Telegram паттерн: сначала скрыть клавиатуру, затем показать emoji. + * + * 🔥 КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Показываем emoji СРАЗУ! + * Не ждем закрытия клавиатуры - emoji начинает выезжать синхронно. */ fun requestShowEmoji( hideKeyboard: () -> Unit, @@ -84,40 +86,25 @@ class KeyboardTransitionCoordinator { Log.d(TAG, " 📊 Current state:") Log.d(TAG, " - currentState=$currentState") Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight") - Log.d(TAG, " - isTransitioning=$isTransitioning") - Log.d(TAG, " - isKeyboardVisible=$isKeyboardVisible, isEmojiVisible=$isEmojiVisible") - Log.d(TAG, " - pendingShowEmojiCallback=${if (pendingShowEmojiCallback != null) "EXISTS" else "null"}") + Log.d(TAG, " - maxKeyboardHeight=$maxKeyboardHeight") - Log.d(TAG, " 📝 Setting currentState = KEYBOARD_TO_EMOJI") currentState = TransitionState.KEYBOARD_TO_EMOJI isTransitioning = true - // 🔥 Сохраняем коллбэк для вызова после закрытия клавиатуры - Log.d(TAG, " 💾 Saving pending emoji callback...") - pendingShowEmojiCallback = { - try { - Log.d(TAG, "🎬 Executing pending emoji show callback") - Log.d(TAG, " Current keyboard height at execution: $keyboardHeight") - Log.d(TAG, " 📞 Calling showEmoji()...") - showEmoji() - Log.d(TAG, " 📞 Setting isEmojiVisible = true") - isEmojiVisible = true - isKeyboardVisible = false - currentState = TransitionState.IDLE - isTransitioning = false - pendingShowEmojiCallback = null - Log.d(TAG, "✅ Emoji shown via pending callback") - } catch (e: Exception) { - Log.e(TAG, "❌ Error in pendingShowEmojiCallback", e) - currentState = TransitionState.IDLE - isTransitioning = false - pendingShowEmojiCallback = null - } + // 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры) + if (maxKeyboardHeight > 0.dp) { + emojiHeight = maxKeyboardHeight + Log.d(TAG, " 📌 Locked emojiHeight to maxKeyboardHeight: $emojiHeight") } - Log.d(TAG, " ✅ Pending callback saved") - // Шаг 1: Скрыть системную клавиатуру - Log.d(TAG, " 📞 Calling hideKeyboard()...") + // 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры + Log.d(TAG, " 🚀 IMMEDIATELY showing emoji (no waiting for keyboard close)") + showEmoji() + isEmojiVisible = true + Log.d(TAG, " ✅ showEmoji() called, isEmojiVisible=true") + + // Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji) + Log.d(TAG, " ⌨️ Hiding keyboard...") try { hideKeyboard() Log.d(TAG, " ✅ hideKeyboard() completed") @@ -125,12 +112,14 @@ class KeyboardTransitionCoordinator { Log.e(TAG, "❌ Error hiding keyboard", e) } - // Если клавиатура уже закрыта (height = 0), показываем emoji сразу - if (keyboardHeight <= 0.dp) { - Log.d(TAG, "⚡ Keyboard already closed, showing emoji immediately") - pendingShowEmojiCallback?.invoke() - } - // Иначе ждем когда updateKeyboardHeight получит 0 + isKeyboardVisible = false + currentState = TransitionState.IDLE + isTransitioning = false + + // Очищаем pending callback - больше не нужен + pendingShowEmojiCallback = null + + Log.d(TAG, "✅ requestShowEmoji() completed") } // ============ Главный метод: Emoji → Keyboard ============ @@ -293,27 +282,13 @@ class KeyboardTransitionCoordinator { Log.d(TAG, " After: emojiHeight restored to $maxKeyboardHeight") } - // Обнуляем keyboardHeight только после восстановления emoji + // Обнуляем keyboardHeight keyboardHeight = 0.dp } else { Log.d(TAG, "⏭️ No update needed (height too small or unchanged)") } - // 🔥 Если клавиатура закрылась И есть pending коллбэк → показываем emoji - if (height == 0.dp && pendingShowEmojiCallback != null) { - Log.d(TAG, "🎯 Keyboard closed (0dp), triggering pending emoji callback") - val callback = pendingShowEmojiCallback - pendingShowEmojiCallback = null // 🔥 Сразу обнуляем чтобы не вызвать дважды - - // Вызываем через небольшую задержку чтобы UI стабилизировался - Handler(Looper.getMainLooper()).postDelayed({ - try { - callback?.invoke() - } catch (e: Exception) { - Log.e(TAG, "❌ Error invoking pending emoji callback", e) - } - }, 50) - } + // 🔥 УБРАН pending callback - теперь emoji показывается сразу в requestShowEmoji() } /** @@ -328,15 +303,29 @@ class KeyboardTransitionCoordinator { /** * Синхронизировать высоты (emoji = keyboard). + * + * 🔥 ВАЖНО: Синхронизируем ТОЛЬКО когда клавиатура ОТКРЫВАЕТСЯ! + * При закрытии клавиатуры emojiHeight должна оставаться фиксированной! */ fun syncHeights() { - Log.d(TAG, "🔄 syncHeights called: keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight") - if (keyboardHeight > 100.dp) { - val oldEmojiHeight = emojiHeight + // 🔥 Синхронизируем ТОЛЬКО если клавиатура ОТКРЫТА и высота больше текущей emoji + if (keyboardHeight > 100.dp && keyboardHeight > emojiHeight) { + Log.d(TAG, "🔄 syncHeights: updating emoji $emojiHeight → $keyboardHeight") emojiHeight = keyboardHeight - Log.d(TAG, "✅ Heights synced: $oldEmojiHeight → $keyboardHeight") } else { - Log.d(TAG, "⏭️ Keyboard height too small ($keyboardHeight), skipping sync") + Log.d(TAG, "⏭️ syncHeights: skipped (keyboard=$keyboardHeight, emoji=$emojiHeight)") + } + } + + /** + * Инициализация высоты emoji панели (для pre-rendered подхода). + * Должна быть вызвана при старте для избежания 0dp высоты. + */ + fun initializeEmojiHeight(height: Dp) { + if (emojiHeight == 0.dp && height > 0.dp) { + Log.d(TAG, "🚀 Initializing emoji height: $height") + emojiHeight = height + maxKeyboardHeight = height } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 0f4d858..24af007 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -71,6 +72,7 @@ import com.rosetta.messenger.ui.components.AppleEmojiTextField import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.onboarding.PrimaryBlue import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator +import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition import android.view.inputmethod.InputMethodManager import android.content.Context @@ -219,6 +221,8 @@ fun ChatDetailScreen( onUserProfileClick: () -> Unit = {}, viewModel: ChatViewModel = viewModel() ) { + // 🔥 ОПТИМИЗАЦИЯ: Убрано логирование из композиции чтобы не засорять logcat + val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current @@ -247,26 +251,36 @@ fun ChatDetailScreen( // 🔥 Emoji picker state (поднят из MessageInputBar для KeyboardAvoidingView) var showEmojiPicker by remember { mutableStateOf(false) } - // Высота эмодзи панели - берём высоту клавиатуры если она открыта, иначе 280dp - val imeInsets = WindowInsets.ime - val imeHeight = with(density) { imeInsets.getBottom(density).toDp() } - val isKeyboardVisible = imeHeight > 50.dp - // 🔥 Запоминаем высоту клавиатуры когда она открыта - var savedKeyboardHeight by remember { mutableStateOf(280.dp) } - LaunchedEffect(imeHeight) { - if (imeHeight > 50.dp) { - savedKeyboardHeight = imeHeight + // 🎯 Координатор плавных переходов клавиатуры (Telegram-style) + val coordinator = rememberKeyboardTransitionCoordinator() + + // 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую в композиции! + // Используем snapshotFlow чтобы избежать рекомпозиции на каждый пиксель анимации + val imeInsets = WindowInsets.ime + + // 🔥 Синхронизируем coordinator с IME высотой через snapshotFlow (БЕЗ рекомпозиции!) + LaunchedEffect(Unit) { + snapshotFlow { + with(density) { imeInsets.getBottom(density).toDp() } + }.collect { currentImeHeight -> + coordinator.updateKeyboardHeight(currentImeHeight) + if (currentImeHeight > 100.dp) { + coordinator.syncHeights() + } } } - // 🔥 Высота панели эмодзи = сохранённая высота клавиатуры (минимум 280.dp) - val emojiPanelHeight = maxOf(savedKeyboardHeight, 280.dp) - // 🔥 Флаг видимости панели эмодзи (тот же что в MessageInputBar) - единый источник правды - val isEmojiPanelVisible = showEmojiPicker && !isKeyboardVisible - - // � Простой отступ без анимации - AnimatedVisibility сама анимирует - val emojiPanelPadding = if (isEmojiPanelVisible) emojiPanelHeight else 0.dp + // 🔥 Инициализируем высоту emoji панели из сохранённой высоты клавиатуры + LaunchedEffect(Unit) { + val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context) + if (savedHeightPx > 0) { + val savedHeightDp = with(density) { savedHeightPx.toDp() } + coordinator.initializeEmojiHeight(savedHeightDp) + } else { + coordinator.initializeEmojiHeight(280.dp) // fallback + } + } // 🔥 Reply/Forward state val replyMessages by viewModel.replyMessages.collectAsState() @@ -736,7 +750,7 @@ fun ChatDetailScreen( // Выпадающее меню - чистый дизайн без артефактов MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( - surface = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White + surface = inputBackgroundColor ) ) { DropdownMenu( @@ -745,7 +759,7 @@ fun ChatDetailScreen( modifier = Modifier .width(220.dp) .background( - color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White, + color = inputBackgroundColor, shape = RoundedCornerShape(16.dp) ) ) { @@ -775,7 +789,8 @@ fun ChatDetailScreen( showMenu = false showDeleteConfirm = true }, - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier.padding(horizontal = 8.dp) + .background(inputBackgroundColor), colors = MenuDefaults.itemColors( textColor = Color(0xFFE53935) ) @@ -822,7 +837,8 @@ fun ChatDetailScreen( showBlockConfirm = true } }, - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier.padding(horizontal = 8.dp) + .background(inputBackgroundColor), colors = MenuDefaults.itemColors( textColor = PrimaryBlue ) @@ -863,7 +879,8 @@ fun ChatDetailScreen( showMenu = false showLogs = true }, - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier.padding(horizontal = 8.dp) + .background(inputBackgroundColor), colors = MenuDefaults.itemColors( textColor = textColor ) @@ -937,7 +954,9 @@ fun ChatDetailScreen( showEmojiPicker = showEmojiPicker, onToggleEmojiPicker = { showEmojiPicker = it }, // Focus requester для автофокуса при reply - focusRequester = inputFocusRequester + focusRequester = inputFocusRequester, + // Coordinator для плавных переходов + coordinator = coordinator ) } } @@ -1978,8 +1997,12 @@ private fun MessageInputBar( showEmojiPicker: Boolean = false, onToggleEmojiPicker: (Boolean) -> Unit = {}, // Focus requester для автофокуса при reply - focusRequester: FocusRequester? = null + focusRequester: FocusRequester? = null, + // Coordinator для плавных переходов клавиатуры + coordinator: KeyboardTransitionCoordinator ) { + // 🔥 ОПТИМИЗАЦИЯ: Убрано логирование чтобы не засорять logcat + val hasReply = replyMessages.isNotEmpty() val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -1991,9 +2014,6 @@ private fun MessageInputBar( val view = LocalView.current val density = LocalDensity.current - // 🎯 Координатор плавных переходов клавиатуры (Telegram-style) - val coordinator = rememberKeyboardTransitionCoordinator() - // 🔥 Ссылка на EditText для программного фокуса var editTextView by remember { mutableStateOf(null) } @@ -2021,56 +2041,47 @@ private fun MessageInputBar( } } - // 🔥 Отслеживаем высоту клавиатуры (Telegram-style) + // 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую - это вызывает рекомпозицию! val imeInsets = WindowInsets.ime - val imeHeight = with(density) { imeInsets.getBottom(density).toDp() } - val isKeyboardVisible = imeHeight > 50.dp // 🔥 Согласованный порог с ChatDetailScreen - // 🔥 Флаг "клавиатура в процессе анимации" - var isKeyboardAnimating by remember { mutableStateOf(false) } + // 🔥 Флаг "клавиатура видна" - обновляется через snapshotFlow, НЕ вызывает рекомпозицию + var isKeyboardVisible by remember { mutableStateOf(false) } + var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) } - // 🔥 Логирование изменений высоты клавиатуры + обновление coordinator - LaunchedEffect(imeHeight) { - android.util.Log.d("KeyboardHeight", "═══════════════════════════════════════════════════════") - android.util.Log.d("KeyboardHeight", "📊 IME height changed: $imeHeight") - android.util.Log.d("KeyboardHeight", " isKeyboardVisible=$isKeyboardVisible") - android.util.Log.d("KeyboardHeight", " showEmojiPicker=$showEmojiPicker") - android.util.Log.d("KeyboardHeight", " isKeyboardAnimating=$isKeyboardAnimating") - - // Обновляем coordinator с актуальной высотой клавиатуры - android.util.Log.d("KeyboardHeight", "🔄 Updating coordinator...") - coordinator.updateKeyboardHeight(imeHeight) - - // Синхронизируем высоту emoji с клавиатурой - if (imeHeight > 100.dp) { - android.util.Log.d("KeyboardHeight", "🔄 Syncing heights...") - coordinator.syncHeights() + // 🔥 Обновляем coordinator через snapshotFlow (БЕЗ рекомпозиции!) + LaunchedEffect(Unit) { + snapshotFlow { + with(density) { imeInsets.getBottom(density).toDp() } + }.collect { currentImeHeight -> + // Обновляем флаг видимости (это НЕ вызывает рекомпозицию напрямую) + isKeyboardVisible = currentImeHeight > 50.dp + + // Обновляем coordinator + coordinator.updateKeyboardHeight(currentImeHeight) + if (currentImeHeight > 100.dp) { + coordinator.syncHeights() + lastStableKeyboardHeight = currentImeHeight + } } - android.util.Log.d("KeyboardHeight", "✅ Coordinator updated") } // 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences) LaunchedEffect(Unit) { // Загружаем сохранённую высоту при старте - val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context) - android.util.Log.d("KeyboardHeight", "📱 MessageInputBar initialized, loaded height: ${savedHeightPx}px") + com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context) } // 🔥 Сохраняем высоту ТОЛЬКО когда клавиатура стабильна - // Используем отдельный LaunchedEffect который НЕ реагирует на каждое изменение LaunchedEffect(isKeyboardVisible, showEmojiPicker) { // Если клавиатура стала видимой и emoji закрыт - if (isKeyboardVisible && !showEmojiPicker && !isKeyboardAnimating) { + if (isKeyboardVisible && !showEmojiPicker) { // Ждем стабилизации - isKeyboardAnimating = true - kotlinx.coroutines.delay(300) // Анимация клавиатуры ~250ms - isKeyboardAnimating = false + kotlinx.coroutines.delay(350) // Анимация клавиатуры ~300ms // Сохраняем только если всё еще видна и emoji закрыт - if (isKeyboardVisible && !showEmojiPicker && imeHeight > 300.dp) { - val heightPx = with(density) { imeHeight.toPx().toInt() } + if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) { + val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() } com.rosetta.messenger.ui.components.KeyboardHeightProvider.saveKeyboardHeight(context, heightPx) - android.util.Log.d("KeyboardHeight", "✅ Stable keyboard height saved: ${imeHeight} (${heightPx}px)") } } } @@ -2343,8 +2354,6 @@ private fun MessageInputBar( android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji") android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...") onToggleEmojiPicker(false) - android.util.Log.d("EmojiPicker", " 📞 Setting coordinator.isEmojiVisible = false...") - coordinator.isEmojiVisible = false android.util.Log.d("EmojiPicker", " ✅ Emoji close requested") } else if (hasFocus && !showEmojiPicker) { android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")