# 🎯 План плавной смены клавиатур (Telegram-style) ## 📊 Анализ проблемы ### Текущая ситуация: - ✅ Высота клавиатур одинаковая (364.95dp) - ❌ UI дергается при переключении клавиатур - ❌ Нет синхронизации анимаций - ❌ Emoji панель появляется/исчезает резко ### Как работает Telegram: **Ключевые компоненты:** 1. **AdjustPanLayoutHelper** - координирует анимации клавиатуры 2. **EditTextEmoji.showPopup()** - управляет переключением 3. **ValueAnimator** - анимирует translationY 4. **250ms duration + keyboardInterpolator** (FastOutSlowIn) **Важные паттерны Telegram:** ```java // 1. Синхронная анимация - emoji панель двигается вместе с keyboard animator.setDuration(AdjustPanLayoutHelper.keyboardDuration); // 250ms animator.setInterpolator(AdjustPanLayoutHelper.keyboardInterpolator); // 2. TranslationY вместо изменения height emojiView.setTranslationY(v); // плавно двигаем панель // 3. Одновременное скрытие keyboard и показ emoji if (!keyboardVisible && !emojiWasVisible) { // анимируем появление emoji снизу ValueAnimator animator = ValueAnimator.ofFloat(emojiPadding, 0); } ``` ## 🎬 Архитектура решения для Compose ### Phase 1: Механизм синхронизации анимаций #### 1.1 KeyboardTransitionCoordinator **Цель:** Координировать переходы между клавиатурами ```kotlin @Composable fun rememberKeyboardTransitionCoordinator(): KeyboardTransitionCoordinator { return remember { KeyboardTransitionCoordinator() } } class KeyboardTransitionCoordinator { // Состояния перехода enum class TransitionState { IDLE, // Ничего не происходит KEYBOARD_TO_EMOJI, // Keyboard → Emoji EMOJI_TO_KEYBOARD, // Emoji → Keyboard KEYBOARD_OPENING, // Только keyboard открывается EMOJI_OPENING, // Только emoji открывается KEYBOARD_CLOSING, // Только keyboard закрывается EMOJI_CLOSING // Только emoji закрывается } var currentState by mutableStateOf(TransitionState.IDLE) var transitionProgress by mutableFloatStateOf(0f) // Высоты для расчетов var keyboardHeight by mutableStateOf(0.dp) var emojiHeight by mutableStateOf(0.dp) // Флаги var isKeyboardVisible by mutableStateOf(false) var isEmojiVisible by mutableStateOf(false) // Запуск перехода fun startTransition( from: TransitionState, to: TransitionState, onComplete: () -> Unit ) { currentState = to // Логика анимации } } ``` #### 1.2 Compose Animatable для плавности ```kotlin val emojiOffsetY = remember { Animatable(0f) } val keyboardOffsetY = remember { Animatable(0f) } // Telegram-style timing val transitionSpec = tween( durationMillis = 250, easing = FastOutSlowInEasing // keyboardInterpolator ) ``` ### Phase 2: Управление состояниями #### 2.1 State Machine для переходов ```kotlin sealed class KeyboardState { object Closed : KeyboardState() object SystemKeyboardOpen : KeyboardState() object EmojiKeyboardOpen : KeyboardState() data class Transitioning( val from: KeyboardState, val to: KeyboardState, val progress: Float ) : KeyboardState() } class KeyboardStateManager { var currentState by mutableStateOf(KeyboardState.Closed) fun transition(target: KeyboardState) { when (currentState to target) { KeyboardState.SystemKeyboardOpen to KeyboardState.EmojiKeyboardOpen -> { // Keyboard → Emoji startKeyboardToEmojiTransition() } KeyboardState.EmojiKeyboardOpen to KeyboardState.SystemKeyboardOpen -> { // Emoji → Keyboard startEmojiToKeyboardTransition() } // ... другие переходы } } } ``` ### Phase 3: Анимация переходов #### 3.1 Keyboard → Emoji (самый сложный) **Проблема:** Системная клавиатура закрывается асинхронно **Решение Telegram:** 1. **Не ждать закрытия клавиатуры** 2. **Сразу показать emoji панель на месте клавиатуры** 3. **Анимировать emoji снизу вверх, пока keyboard уходит** ```kotlin @Composable fun KeyboardToEmojiTransition( coordinator: KeyboardTransitionCoordinator, onComplete: () -> Unit ) { val offsetY = remember { Animatable(coordinator.keyboardHeight.value) } LaunchedEffect(Unit) { // Шаг 1: Скрыть клавиатуру (асинхронно) coordinator.hideSystemKeyboard() // Шаг 2: Сразу анимировать emoji снизу (250ms) offsetY.animateTo( targetValue = 0f, animationSpec = tween( durationMillis = 250, easing = FastOutSlowInEasing ) ) onComplete() } // Emoji панель с offset Box( modifier = Modifier .fillMaxWidth() .height(coordinator.emojiHeight) .offset(y = offsetY.value.dp) ) { OptimizedEmojiPicker(...) } } ``` #### 3.2 Emoji → Keyboard ```kotlin @Composable fun EmojiToKeyboardTransition( coordinator: KeyboardTransitionCoordinator, onComplete: () -> Unit ) { val emojiOffsetY = remember { Animatable(0f) } LaunchedEffect(Unit) { // Шаг 1: Показать клавиатуру (асинхронно) coordinator.showSystemKeyboard() // Шаг 2: Одновременно анимировать emoji вниз (250ms) emojiOffsetY.animateTo( targetValue = coordinator.emojiHeight.value, animationSpec = tween( durationMillis = 250, easing = FastOutLinearInEasing // быстрее уходит ) ) onComplete() } // Emoji панель уходит вниз Box( modifier = Modifier .fillMaxWidth() .height(coordinator.emojiHeight) .offset(y = emojiOffsetY.value.dp) .alpha(1f - (emojiOffsetY.value / coordinator.emojiHeight.value)) ) { OptimizedEmojiPicker(...) } } ``` ### Phase 4: Интеграция с IME #### 4.1 WindowInsets синхронизация ```kotlin @Composable fun MessageInputBar() { val coordinator = rememberKeyboardTransitionCoordinator() val ime = WindowInsets.ime val imeHeight = ime.getBottom(LocalDensity.current).toDp() // Отслеживаем изменения IME LaunchedEffect(imeHeight) { coordinator.keyboardHeight = imeHeight // Если IME закрылась во время показа emoji if (imeHeight == 0.dp && coordinator.isEmojiVisible) { // Продолжить показ emoji без дерганья coordinator.currentState = TransitionState.IDLE } } } ``` #### 4.2 Spacer для резервации места **Ключевая идея Telegram:** Emoji панель резервирует место клавиатуры ```kotlin @Composable fun KeyboardSpacer( coordinator: KeyboardTransitionCoordinator ) { val height by animateDpAsState( targetValue = when { coordinator.isKeyboardVisible -> coordinator.keyboardHeight coordinator.isEmojiVisible -> coordinator.emojiHeight else -> 0.dp }, animationSpec = tween( durationMillis = 250, easing = FastOutSlowInEasing ) ) Spacer(modifier = Modifier.height(height)) } ``` ## 🔧 Детальная имплементация ### Шаг 1: KeyboardTransitionCoordinator.kt ```kotlin class KeyboardTransitionCoordinator( private val context: Context ) { companion object { const val TRANSITION_DURATION = 250L val TRANSITION_EASING = FastOutSlowInEasing } // Состояние var keyboardHeight by mutableStateOf(0.dp) var emojiHeight by mutableStateOf(0.dp) var isKeyboardVisible by mutableStateOf(false) var isEmojiVisible by mutableStateOf(false) var isTransitioning by mutableStateOf(false) // Для отладки private val tag = "KeyboardTransition" fun requestShowEmoji( hideKeyboard: () -> Unit, showEmoji: () -> Unit ) { Log.d(tag, "🔄 Keyboard → Emoji transition started") isTransitioning = true // Telegram паттерн: сначала скрыть клавиатуру hideKeyboard() // Через небольшую задержку показать emoji // (даем системе начать закрытие клавиатуры) Handler(Looper.getMainLooper()).postDelayed({ showEmoji() isEmojiVisible = true isKeyboardVisible = false isTransitioning = false Log.d(tag, "✅ Emoji visible, keyboard hidden") }, 50L) } fun requestShowKeyboard( showKeyboard: () -> Unit, hideEmoji: () -> Unit ) { Log.d(tag, "🔄 Emoji → Keyboard transition started") isTransitioning = true // Сначала показать клавиатуру showKeyboard() // Emoji скрыть после начала анимации Handler(Looper.getMainLooper()).postDelayed({ hideEmoji() isEmojiVisible = false isKeyboardVisible = true isTransitioning = false Log.d(tag, "✅ Keyboard visible, emoji hidden") }, 50L) } } ``` ### Шаг 2: Обновить MessageInputBar ```kotlin @Composable fun MessageInputBar(...) { val coordinator = rememberKeyboardTransitionCoordinator() val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current // Отслеживание IME val ime = WindowInsets.ime val imeHeight = with(LocalDensity.current) { ime.getBottom(this).toDp() } // Обновляем высоту клавиатуры LaunchedEffect(imeHeight) { if (imeHeight > 100.dp) { coordinator.keyboardHeight = imeHeight coordinator.isKeyboardVisible = true } else if (imeHeight == 0.dp && !coordinator.isEmojiVisible) { coordinator.isKeyboardVisible = false } Log.d("KeyboardHeight", "IME height: $imeHeight") } Column { // Input field Row { TextField(...) // Emoji button IconButton( onClick = { if (coordinator.isEmojiVisible) { // Переключить на клавиатуру coordinator.requestShowKeyboard( showKeyboard = { focusRequester.requestFocus() keyboardController?.show() }, hideEmoji = { showEmojiPicker = false } ) } else { // Переключить на emoji coordinator.requestShowEmoji( hideKeyboard = { keyboardController?.hide() }, showEmoji = { showEmojiPicker = true coordinator.emojiHeight = coordinator.keyboardHeight } ) } } ) { Icon( imageVector = if (coordinator.isEmojiVisible) { Icons.Default.Keyboard } else { Icons.Default.EmojiEmotions } ) } } // Emoji picker с анимацией AnimatedKeyboardTransition( coordinator = coordinator, showEmojiPicker = showEmojiPicker ) } } ``` ### Шаг 3: AnimatedKeyboardTransition.kt ```kotlin @Composable fun AnimatedKeyboardTransition( coordinator: KeyboardTransitionCoordinator, showEmojiPicker: Boolean ) { val offsetY = remember { Animatable(0f) } val alpha = remember { Animatable(0f) } LaunchedEffect(showEmojiPicker) { if (showEmojiPicker) { // Показать emoji launch { offsetY.snapTo(coordinator.emojiHeight.value) offsetY.animateTo( targetValue = 0f, animationSpec = tween( durationMillis = 250, easing = FastOutSlowInEasing ) ) } launch { alpha.snapTo(0f) alpha.animateTo( targetValue = 1f, animationSpec = tween(durationMillis = 200) ) } } else { // Скрыть emoji launch { offsetY.animateTo( targetValue = coordinator.emojiHeight.value, animationSpec = tween( durationMillis = 200, easing = FastOutLinearInEasing ) ) } launch { alpha.animateTo( targetValue = 0f, animationSpec = tween(durationMillis = 150) ) } } } if (showEmojiPicker || offsetY.value > 0f) { Box( modifier = Modifier .fillMaxWidth() .height(coordinator.emojiHeight) .offset(y = offsetY.value.dp) .alpha(alpha.value) ) { OptimizedEmojiPicker( keyboardHeight = coordinator.emojiHeight, ... ) } } } ``` ## 📈 Этапы внедрения ### Этап 1: Базовая инфраструктура (1-2 часа) - [ ] Создать `KeyboardTransitionCoordinator.kt` - [ ] Добавить state management для переходов - [ ] Настроить логирование для отладки ### Этап 2: Анимации (2-3 часа) - [ ] Создать `AnimatedKeyboardTransition.kt` - [ ] Реализовать Keyboard → Emoji переход - [ ] Реализовать Emoji → Keyboard переход - [ ] Добавить fade анимацию для плавности ### Этап 3: Интеграция (1-2 часа) - [ ] Обновить `MessageInputBar` с coordinator - [ ] Интегрировать с `OptimizedEmojiPicker` - [ ] Добавить `KeyboardSpacer` для резервации места ### Этап 4: Полировка (1-2 часа) - [ ] Синхронизация с IME events - [ ] Обработка edge cases (поворот экрана, многозадачность) - [ ] Убрать дерганья при быстрых переключениях - [ ] Тестирование на разных устройствах ### Этап 5: Оптимизация (1 час) - [ ] Убрать избыточные recomposition - [ ] Добавить `remember` где нужно - [ ] Проверить производительность - [ ] Удалить debug логи (опционально) ## 🎨 Ключевые паттерны Telegram ### 1. **Немедленное резервирование места** ```kotlin // Telegram сразу резервирует место для emoji панели emojiPadding = currentHeight sizeNotifierLayout.requestLayout() ``` ### 2. **TranslationY вместо show/hide** ```kotlin // Анимация позиции, а не visibility emojiView.setTranslationY(v) ``` ### 3. **Синхронные анимации** ```kotlin // Все анимации используют одинаковый timing animator.setDuration(AdjustPanLayoutHelper.keyboardDuration); // 250ms animator.setInterpolator(AdjustPanLayoutHelper.keyboardInterpolator); ``` ### 4. **Плавный alpha для emoji** ```kotlin // Fade in/out для естественности emojiViewAlpha = 1f - v / (float) emojiPadding; emojiView.setAlpha(emojiViewAlpha); ``` ## 🐛 Возможные проблемы и решения ### Проблема 1: IME закрывается слишком медленно **Решение:** Не ждать закрытия, показать emoji сразу ```kotlin // Telegram паттерн if (!keyboardVisible && !emojiWasVisible) { // Анимировать emoji снизу, пока keyboard закрывается } ``` ### Проблема 2: UI дергается при переключении **Решение:** Резервировать место спейсером ```kotlin Spacer(modifier = Modifier.height(max(keyboardHeight, emojiHeight))) ``` ### Проблема 3: Разные высоты на landscape/portrait **Решение:** Сохранять отдельно для каждой ориентации ```kotlin val keyboardHeight = if (isLandscape) keyboardHeightLand else keyboardHeightPort ``` ### Проблема 4: Быстрые переключения **Решение:** Отменять предыдущие анимации ```kotlin LaunchedEffect(showEmojiPicker) { offsetY.stop() // Остановить текущую анимацию // Начать новую } ``` ## 📊 Ожидаемый результат ### До: - ❌ UI дергается - ❌ Резкое появление/исчезновение - ❌ Несинхронные анимации - ❌ Пустое место при переключении ### После: - ✅ Плавный переход за 250ms - ✅ Синхронизированные анимации - ✅ Telegram-style UX - ✅ Нет дерганий UI - ✅ Резервированное место ## 🚀 Готовы начинать? План готов! Начнем с создания `KeyboardTransitionCoordinator.kt` - это основа всей системы. После этого добавим анимации и интегрируем в `MessageInputBar`. Telegram решает эту задачу через: 1. **ValueAnimator** с 250ms и FastOutSlowIn 2. **TranslationY** для плавного движения 3. **Немедленное резервирование места** 4. **Синхронизация всех анимаций** Готов приступить к реализации! 🎯