diff --git a/SMOOTH_KEYBOARD_TRANSITION_PLAN.md b/SMOOTH_KEYBOARD_TRANSITION_PLAN.md new file mode 100644 index 0000000..fb697f1 --- /dev/null +++ b/SMOOTH_KEYBOARD_TRANSITION_PLAN.md @@ -0,0 +1,621 @@ +# 🎯 План плавной смены клавиатур (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. **Синхронизация всех анимаций** + +Готов приступить к реализации! 🎯 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 new file mode 100644 index 0000000..96d2d5f --- /dev/null +++ b/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt @@ -0,0 +1,171 @@ +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.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.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.dp +import android.util.Log +import kotlinx.coroutines.launch + +/** + * Анимированный контейнер для emoji панели с Telegram-style переходами. + * + * Паттерны Telegram: + * - Keyboard → Emoji: панель выезжает снизу за 250ms (FastOutSlowIn) + * - Emoji → Keyboard: панель уезжает вниз за 200ms (FastOutLinearIn) + * - TranslationY + Alpha для плавности + * - Немедленное резервирование места + */ +@Composable +fun AnimatedKeyboardTransition( + coordinator: KeyboardTransitionCoordinator, + showEmojiPicker: Boolean, + content: @Composable () -> Unit +) { + 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}") + } + } + } + + // 🔥 Рендерим контент ТОЛЬКО если showEmojiPicker = true + // Не проверяем offsetY, чтобы избежать показа emoji при открытии клавиатуры + if (showEmojiPicker) { + Log.d(tag, "✅ Rendering emoji content (showEmojiPicker=true)") + Box( + modifier = Modifier + .fillMaxWidth() + .height(coordinator.emojiHeight) + .offset(y = offsetY.value.dp) + .alpha(alpha.value) + ) { + content() + } + } +} + +/** + * Упрощенная версия без fade анимации. + * Только slide (как в оригинальном Telegram). + */ +@Composable +fun SimpleAnimatedKeyboardTransition( + coordinator: KeyboardTransitionCoordinator, + 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() + } + } +} 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 new file mode 100644 index 0000000..a0ff857 --- /dev/null +++ b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt @@ -0,0 +1,374 @@ +package app.rosette.android.ui.keyboard + +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Координатор переходов между системной клавиатурой и emoji панелью. + * Реализует Telegram-style плавные анимации. + * + * Ключевые принципы: + * - 250ms duration (как в Telegram AdjustPanLayoutHelper) + * - Немедленное резервирование места + * - TranslationY анимации вместо show/hide + * - Синхронизация всех переходов + */ +class KeyboardTransitionCoordinator { + + companion object { + const val TRANSITION_DURATION = 250L + const val SHORT_DELAY = 50L + private const val TAG = "KeyboardTransition" + } + + // ============ Состояния переходов ============ + + 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) + private set + + var transitionProgress by mutableFloatStateOf(0f) + private set + + // ============ Высоты ============ + + 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 set + + // 🔥 Коллбэк для показа emoji (сохраняем для вызова после закрытия клавиатуры) + private var pendingShowEmojiCallback: (() -> Unit)? = null + + // ============ Главный метод: Keyboard → Emoji ============ + + /** + * Переход от системной клавиатуры к emoji панели. + * Telegram паттерн: сначала скрыть клавиатуру, затем показать emoji. + */ + fun requestShowEmoji( + hideKeyboard: () -> Unit, + showEmoji: () -> Unit + ) { + Log.d(TAG, "═══════════════════════════════════════════════════════") + Log.d(TAG, "📱 requestShowEmoji() START") + Log.d(TAG, "🔄 Keyboard → Emoji transition started") + 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, " 📝 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 + } + } + Log.d(TAG, " ✅ Pending callback saved") + + // Шаг 1: Скрыть системную клавиатуру + Log.d(TAG, " 📞 Calling hideKeyboard()...") + try { + hideKeyboard() + Log.d(TAG, " ✅ hideKeyboard() completed") + } catch (e: Exception) { + 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 + } + + // ============ Главный метод: Emoji → Keyboard ============ + + /** + * Переход от emoji панели к системной клавиатуре. + * Telegram паттерн: показать клавиатуру и плавно скрыть emoji. + */ + fun requestShowKeyboard( + showKeyboard: () -> Unit, + hideEmoji: () -> Unit + ) { + Log.d(TAG, "═══════════════════════════════════════════════════════") + Log.d(TAG, "⌨️ requestShowKeyboard() START") + Log.d(TAG, "🔄 Emoji → Keyboard transition started") + 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"}") + + // 🔥 Отменяем pending emoji callback если он есть (предотвращаем конфликт) + if (pendingShowEmojiCallback != null) { + Log.d(TAG, "⚠️ Cancelling pending emoji callback (switching to keyboard)") + pendingShowEmojiCallback = null + } + + Log.d(TAG, " 📞 Setting currentState = EMOJI_TO_KEYBOARD") + currentState = TransitionState.EMOJI_TO_KEYBOARD + isTransitioning = true + + // Шаг 1: Показать системную клавиатуру + Log.d(TAG, " 📞 Calling showKeyboard()...") + try { + showKeyboard() + Log.d(TAG, " ✅ showKeyboard() completed") + } catch (e: Exception) { + Log.e(TAG, "❌ Error showing keyboard", e) + } + + // Шаг 2: Через небольшую задержку скрыть emoji + Handler(Looper.getMainLooper()).postDelayed({ + try { + hideEmoji() + isEmojiVisible = false + isKeyboardVisible = true + + // Через время анимации завершаем переход + Handler(Looper.getMainLooper()).postDelayed({ + currentState = TransitionState.IDLE + isTransitioning = false + Log.d(TAG, "✅ Keyboard visible, emoji hidden") + }, TRANSITION_DURATION) + } catch (e: Exception) { + Log.e(TAG, "❌ Error in requestShowKeyboard transition", e) + currentState = TransitionState.IDLE + isTransitioning = false + } + }, SHORT_DELAY) + } + + // ============ Простые переходы ============ + + /** + * Открыть только emoji панель (без клавиатуры). + */ + fun openEmojiOnly(showEmoji: () -> Unit) { + Log.d(TAG, "😊 Opening emoji panel only") + + currentState = TransitionState.EMOJI_OPENING + isTransitioning = true + + // Установить высоту emoji равной сохраненной высоте клавиатуры + if (emojiHeight == 0.dp && keyboardHeight > 0.dp) { + emojiHeight = keyboardHeight + } + + showEmoji() + isEmojiVisible = true + + Handler(Looper.getMainLooper()).postDelayed({ + currentState = TransitionState.IDLE + isTransitioning = false + Log.d(TAG, "✅ Emoji panel opened") + }, TRANSITION_DURATION) + } + + /** + * Закрыть emoji панель. + */ + fun closeEmoji(hideEmoji: () -> Unit) { + Log.d(TAG, "😊 Closing emoji panel") + + currentState = TransitionState.EMOJI_CLOSING + isTransitioning = true + + hideEmoji() + isEmojiVisible = false + + Handler(Looper.getMainLooper()).postDelayed({ + currentState = TransitionState.IDLE + isTransitioning = false + Log.d(TAG, "✅ Emoji panel closed") + }, TRANSITION_DURATION) + } + + /** + * Закрыть системную клавиатуру. + */ + fun closeKeyboard(hideKeyboard: () -> Unit) { + Log.d(TAG, "⌨️ Closing keyboard") + + currentState = TransitionState.KEYBOARD_CLOSING + isTransitioning = true + + hideKeyboard() + isKeyboardVisible = false + + Handler(Looper.getMainLooper()).postDelayed({ + currentState = TransitionState.IDLE + isTransitioning = false + Log.d(TAG, "✅ Keyboard closed") + }, TRANSITION_DURATION) + } + + // ============ Вспомогательные методы ============ + + /** + * Обновить высоту клавиатуры из IME. + */ + fun updateKeyboardHeight(height: Dp) { + Log.d(TAG, "════════════════════════════════════════════════════════") + Log.d(TAG, "📏 updateKeyboardHeight called: $keyboardHeight → $height") + Log.d(TAG, " isKeyboardVisible=$isKeyboardVisible, emojiHeight=$emojiHeight") + + if (height > 100.dp && height != keyboardHeight) { + Log.d(TAG, "✅ Keyboard height updated: $keyboardHeight → $height") + keyboardHeight = height + + // Если emoji высота не установлена, синхронизировать + if (emojiHeight == 0.dp) { + Log.d(TAG, "🔄 Syncing emoji height (was 0): $emojiHeight → $height") + emojiHeight = height + } + } 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) + } + } + + /** + * Обновить высоту emoji панели. + */ + fun updateEmojiHeight(height: Dp) { + if (height > 0.dp && height != emojiHeight) { + Log.d(TAG, "📏 Emoji height updated: $emojiHeight → $height") + emojiHeight = height + } + } + + /** + * Синхронизировать высоты (emoji = keyboard). + */ + fun syncHeights() { + Log.d(TAG, "🔄 syncHeights called: keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight") + if (keyboardHeight > 100.dp) { + val oldEmojiHeight = emojiHeight + emojiHeight = keyboardHeight + Log.d(TAG, "✅ Heights synced: $oldEmojiHeight → $keyboardHeight") + } else { + Log.d(TAG, "⏭️ Keyboard height too small ($keyboardHeight), skipping sync") + } + } + + /** + * Получить текущую высоту для резервирования места. + * Telegram паттерн: всегда резервировать максимум из двух. + */ + fun getReservedHeight(): Dp { + return when { + isKeyboardVisible -> keyboardHeight + isEmojiVisible -> emojiHeight + isTransitioning -> maxOf(keyboardHeight, emojiHeight) + else -> 0.dp + } + } + + /** + * Проверка, можно ли начать новый переход. + */ + fun canStartTransition(): Boolean { + return !isTransitioning + } + + /** + * Сброс состояния (для отладки). + */ + fun reset() { + Log.d(TAG, "🔄 Reset coordinator state") + currentState = TransitionState.IDLE + isTransitioning = false + isKeyboardVisible = false + isEmojiVisible = false + transitionProgress = 0f + } + + /** + * Логирование текущего состояния. + */ + fun logState() { + Log.d(TAG, """ + 📊 Coordinator State: + - state: $currentState + - transitioning: $isTransitioning + - keyboardVisible: $isKeyboardVisible (height=$keyboardHeight) + - emojiVisible: $isEmojiVisible (height=$emojiHeight) + - progress: $transitionProgress + """.trimIndent()) + } +} + +/** + * Composable для создания и запоминания coordinator'а. + */ +@Composable +fun rememberKeyboardTransitionCoordinator(): KeyboardTransitionCoordinator { + return remember { KeyboardTransitionCoordinator() } +} 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 52c29f1..0f4d858 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 @@ -70,6 +70,8 @@ import com.rosetta.messenger.ui.components.AppleEmojiText 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.AnimatedKeyboardTransition import android.view.inputmethod.InputMethodManager import android.content.Context import androidx.compose.ui.platform.LocalContext @@ -1989,19 +1991,32 @@ private fun MessageInputBar( val view = LocalView.current val density = LocalDensity.current + // 🎯 Координатор плавных переходов клавиатуры (Telegram-style) + val coordinator = rememberKeyboardTransitionCoordinator() + // 🔥 Ссылка на EditText для программного фокуса var editTextView by remember { mutableStateOf(null) } // 🔥 Автофокус при открытии reply панели LaunchedEffect(hasReply, editTextView) { if (hasReply) { + android.util.Log.d("EmojiPicker", "═══════════════════════════════════════════════════════") + android.util.Log.d("EmojiPicker", "💬 Reply panel opened, hasReply=$hasReply") + android.util.Log.d("EmojiPicker", " 📊 editTextView=$editTextView, showEmojiPicker=$showEmojiPicker") // Даём время на создание view если ещё null kotlinx.coroutines.delay(50) editTextView?.let { editText -> - editText.requestFocus() - // Открываем клавиатуру - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + // 🔥 НЕ открываем клавиатуру если emoji уже открыт + if (!showEmojiPicker) { + android.util.Log.d("EmojiPicker", " ⌨️ Requesting focus and keyboard for reply...") + editText.requestFocus() + // Открываем клавиатуру + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + android.util.Log.d("EmojiPicker", " ✅ Auto-opened keyboard for reply") + } else { + android.util.Log.d("EmojiPicker", " ⏭️ Skip auto-keyboard for reply (emoji is open)") + } } } } @@ -2014,9 +2029,24 @@ private fun MessageInputBar( // 🔥 Флаг "клавиатура в процессе анимации" var isKeyboardAnimating by remember { mutableStateOf(false) } - // 🔥 Логирование изменений высоты клавиатуры + // 🔥 Логирование изменений высоты клавиатуры + обновление coordinator LaunchedEffect(imeHeight) { - android.util.Log.d("KeyboardHeight", "📊 IME height: $imeHeight (visible=$isKeyboardVisible, showEmojiPicker=$showEmojiPicker, animating=$isKeyboardAnimating)") + 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() + } + android.util.Log.d("KeyboardHeight", "✅ Coordinator updated") } // 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences) @@ -2067,71 +2097,45 @@ private fun MessageInputBar( focusManager.clearFocus(force = true) } - // 🔥 Функция переключения emoji picker - МАКСИМАЛЬНО АГРЕССИВНОЕ ОТКРЫТИЕ КЛАВИАТУРЫ + // 🔥 Функция переключения emoji picker с Telegram-style transitions fun toggleEmojiPicker() { val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager android.util.Log.d("EmojiPicker", "=".repeat(60)) android.util.Log.d("EmojiPicker", "🔥 toggleEmojiPicker START") - android.util.Log.d("EmojiPicker", " State: showEmojiPicker=$showEmojiPicker, isKeyboardVisible=$isKeyboardVisible") - android.util.Log.d("EmojiPicker", " IME height: $imeHeight, editTextView=${if (editTextView != null) "SET" else "NULL"}") + android.util.Log.d("EmojiPicker", " showEmojiPicker(local)=$showEmojiPicker") + coordinator.logState() - if (showEmojiPicker) { - // ========== ЗАКРЫВАЕМ EMOJI → ОТКРЫВАЕМ КЛАВИАТУРУ ========== - android.util.Log.d("EmojiPicker", "📱 Action: CLOSING emoji → OPENING keyboard") - val startTime = System.currentTimeMillis() - - // Шаг 1: Закрываем emoji панель - onToggleEmojiPicker(false) - android.util.Log.d("EmojiPicker", " [1] Emoji panel closed") - - // Шаг 2: Немедленно фокусируем и открываем клавиатуру - editTextView?.let { editText -> - editText.requestFocus() - - // Метод 1: Немедленный вызов - imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) - android.util.Log.d("EmojiPicker", " [2] Method 1: showSoftInput(FORCED) called") - - // Метод 2: Через post (следующий frame) - view.post { - val elapsed = System.currentTimeMillis() - startTime - imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) - android.util.Log.d("EmojiPicker", " [3] Method 2: showSoftInput(IMPLICIT) called (${elapsed}ms)") - } - - // Метод 3: Через postDelayed (100ms) - view.postDelayed({ - val elapsed = System.currentTimeMillis() - startTime - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) - android.util.Log.d("EmojiPicker", " [4] Method 3: toggleSoftInput called (${elapsed}ms)") - }, 100) - - // Метод 4: Финальная попытка через 200ms - view.postDelayed({ - val elapsed = System.currentTimeMillis() - startTime - if (!isKeyboardVisible) { - android.util.Log.w("EmojiPicker", " [5] ⚠️ Keyboard still not visible after ${elapsed}ms, forcing again") + // 🔥 ИСПОЛЬЗУЕМ coordinator.isEmojiVisible вместо showEmojiPicker для более точного состояния + if (coordinator.isEmojiVisible) { + // ========== EMOJI → KEYBOARD ========== + android.util.Log.d("EmojiPicker", "🔄 Switching: Emoji → Keyboard") + coordinator.requestShowKeyboard( + showKeyboard = { + editTextView?.let { editText -> + editText.requestFocus() imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) - } else { - android.util.Log.d("EmojiPicker", " [5] ✅ Keyboard opened successfully in ${elapsed}ms!") + android.util.Log.d("EmojiPicker", "📱 Keyboard show requested") } - }, 200) - } ?: android.util.Log.e("EmojiPicker", " ❌ ERROR: editTextView is null!") + }, + hideEmoji = { + onToggleEmojiPicker(false) + android.util.Log.d("EmojiPicker", "😊 Emoji panel hidden") + } + ) } else { - // ========== ОТКРЫВАЕМ EMOJI → ЗАКРЫВАЕМ КЛАВИАТУРУ ========== - android.util.Log.d("EmojiPicker", "😊 Action: OPENING emoji → CLOSING keyboard") - - // Шаг 1: Скрываем клавиатуру - imm.hideSoftInputFromWindow(view.windowToken, 0) - android.util.Log.d("EmojiPicker", " [1] Keyboard hide requested") - - // Шаг 2: Небольшая задержка для плавности - view.postDelayed({ - // Шаг 3: Открываем emoji панель - onToggleEmojiPicker(true) - android.util.Log.d("EmojiPicker", " [2] ✅ Emoji panel opened (50ms delay)") - }, 50) + // ========== KEYBOARD → EMOJI ========== + android.util.Log.d("EmojiPicker", "🔄 Switching: Keyboard → Emoji") + coordinator.requestShowEmoji( + hideKeyboard = { + imm.hideSoftInputFromWindow(view.windowToken, 0) + android.util.Log.d("EmojiPicker", "⌨️ Keyboard hide requested") + }, + showEmoji = { + onToggleEmojiPicker(true) + android.util.Log.d("EmojiPicker", "😊 Emoji panel shown") + } + ) } android.util.Log.d("EmojiPicker", "🔥 toggleEmojiPicker END") @@ -2309,26 +2313,6 @@ private fun MessageInputBar( .background( color = backgroundColor // Тот же цвет что и фон чата ) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - // При клике на инпут - закрываем эмодзи панель и открываем клавиатуру - if (showEmojiPicker) { - onToggleEmojiPicker(false) - // Открываем клавиатуру после небольшой задержки - view.postDelayed({ - editTextView?.let { editText -> - editText.requestFocus() - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) - } - }, 100) - } else { - // Просто фокусируем для открытия клавиатуры - editTextView?.requestFocus() - } - } .padding(horizontal = 12.dp, vertical = 8.dp), contentAlignment = Alignment.TopStart // 🔥 TopStart чтобы текст не обрезался ) { @@ -2344,7 +2328,29 @@ private fun MessageInputBar( onViewCreated = { view -> // 🔥 Сохраняем ссылку на EditText для программного открытия клавиатуры editTextView = view - android.util.Log.d("EmojiPicker", "✅ editTextView set: $view") + }, + onFocusChanged = { hasFocus -> + android.util.Log.d("EmojiPicker", "═══════════════════════════════════════════════════════") + android.util.Log.d("EmojiPicker", "🎯 TextField focus changed: hasFocus=$hasFocus") + android.util.Log.d("EmojiPicker", " 📊 Current state:") + android.util.Log.d("EmojiPicker", " - showEmojiPicker=$showEmojiPicker") + android.util.Log.d("EmojiPicker", " - coordinator.isEmojiVisible=${coordinator.isEmojiVisible}") + android.util.Log.d("EmojiPicker", " - coordinator.isKeyboardVisible=${coordinator.isKeyboardVisible}") + android.util.Log.d("EmojiPicker", " - coordinator.currentState=${coordinator.currentState}") + + // Если TextField получил фокус И emoji открыт → закрываем emoji + if (hasFocus && showEmojiPicker) { + 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") + } else if (!hasFocus) { + android.util.Log.d("EmojiPicker", "👋 TextField lost focus") + } } ) } @@ -2391,22 +2397,25 @@ private fun MessageInputBar( } // End of else (not blocked) - // 🔥 ОПТИМИЗИРОВАННЫЙ EMOJI PICKER - с предзагрузкой и smooth animations + // 🔥 EMOJI PICKER с плавными Telegram-style анимациями if (!isBlocked) { - // Новый оптимизированный пикер автоматически управляет анимациями - OptimizedEmojiPicker( - isVisible = showEmojiPicker && !isKeyboardVisible, - isDarkTheme = isDarkTheme, - onEmojiSelected = { emoji -> - onValueChange(value + emoji) - }, - onClose = { - // 🔥 Используем toggleEmojiPicker() - он закроет панель и откроет клавиатуру - toggleEmojiPicker() - }, - modifier = Modifier - .fillMaxWidth() - ) + AnimatedKeyboardTransition( + coordinator = coordinator, + showEmojiPicker = showEmojiPicker + ) { + OptimizedEmojiPicker( + isVisible = true, // Видимость контролирует AnimatedKeyboardTransition + isDarkTheme = isDarkTheme, + onEmojiSelected = { emoji -> + onValueChange(value + emoji) + }, + onClose = { + // Используем coordinator для плавного перехода + toggleEmojiPicker() + }, + modifier = Modifier.fillMaxWidth() + ) + } } // End of if (!isBlocked) for emoji picker } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt index 5ddf356..f1e4281 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt @@ -208,7 +208,8 @@ fun AppleEmojiTextField( hint: String = "Message", hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray, onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null, - requestFocus: Boolean = false + requestFocus: Boolean = false, + onFocusChanged: ((Boolean) -> Unit)? = null ) { // Храним ссылку на view для управления фокусом var editTextView by remember { mutableStateOf(null) } @@ -236,6 +237,14 @@ fun AppleEmojiTextField( setBackgroundColor(android.graphics.Color.TRANSPARENT) // Сохраняем ссылку на view editTextView = this + // Подключаем callback для изменения фокуса + setOnFocusChangeListener { _, hasFocus -> + android.util.Log.d("AppleEmojiTextField", "═══════════════════════════════════════════════════════") + android.util.Log.d("AppleEmojiTextField", "🎯 Native EditText focus changed: hasFocus=$hasFocus") + android.util.Log.d("AppleEmojiTextField", " 📍 Calling onFocusChanged callback...") + onFocusChanged?.invoke(hasFocus) + android.util.Log.d("AppleEmojiTextField", " ✅ onFocusChanged callback completed") + } // Уведомляем о создании view onViewCreated?.invoke(this) } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/OptimizedEmojiPicker.kt b/app/src/main/java/com/rosetta/messenger/ui/components/OptimizedEmojiPicker.kt index c7576eb..b2ca4b3 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/OptimizedEmojiPicker.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/OptimizedEmojiPicker.kt @@ -45,15 +45,15 @@ import kotlinx.coroutines.launch * 2. LazyGrid с оптимизированными настройками (beyondBoundsLayout) * 3. Hardware layer для анимаций * 4. Минимум recomposition (derivedStateOf, remember keys) - * 5. Smooth slide + fade transitions (Telegram-style) - * 6. Coil оптимизация (hardware acceleration, size limits) - * 7. SharedPreferences для сохранения высоты клавиатуры (как в Telegram) - * 8. keyboardDuration для синхронизации с системной клавиатурой + * 5. Coil оптимизация (hardware acceleration, size limits) + * 6. SharedPreferences для сохранения высоты клавиатуры (как в Telegram) + * 7. keyboardDuration для синхронизации с системной клавиатурой + * 8. Анимация управляется внешним AnimatedKeyboardTransition * - * @param isVisible Видимость панели + * @param isVisible Видимость панели (для внутренней логики) * @param isDarkTheme Темная/светлая тема * @param onEmojiSelected Callback при выборе эмодзи - * @param onClose Callback при закрытии (не используется, панель просто скрывается) + * @param onClose Callback при закрытии * @param modifier Модификатор */ @OptIn(ExperimentalAnimationApi::class) @@ -68,59 +68,19 @@ fun OptimizedEmojiPicker( // 🔥 Используем сохранённую высоту клавиатуры (как в Telegram) val savedKeyboardHeight = rememberSavedKeyboardHeight() - // 🔥 Telegram's keyboardDuration для синхронизации анимации - val animationDuration = KeyboardHeightProvider.getKeyboardAnimationDuration().toInt() - // 🔥 Логирование изменений видимости LaunchedEffect(isVisible) { - android.util.Log.d("EmojiPicker", "🎭 OptimizedEmojiPicker visibility changed: $isVisible (height=${savedKeyboardHeight}, animDuration=${animationDuration}ms)") + android.util.Log.d("EmojiPicker", "🎭 OptimizedEmojiPicker visibility: $isVisible (height=${savedKeyboardHeight})") } - // 🎭 Telegram-style анимация: используем сохранённую длительность - AnimatedVisibility( - visible = isVisible, - enter = slideInVertically( - initialOffsetY = { it }, - animationSpec = tween( - durationMillis = animationDuration, // 🔥 Telegram's 250ms - easing = FastOutSlowInEasing - ) - ) + fadeIn( - animationSpec = tween( - durationMillis = animationDuration / 2, - easing = LinearEasing - ) - ), - exit = slideOutVertically( - targetOffsetY = { it }, - animationSpec = tween( - durationMillis = (animationDuration * 0.8).toInt(), // 🔥 Быстрое закрытие (200ms) - easing = FastOutLinearInEasing - ) - ) + fadeOut( - animationSpec = tween( - durationMillis = (animationDuration * 0.6).toInt(), - easing = LinearEasing - ) - ), + // 🔥 Рендерим контент напрямую без AnimatedVisibility + // Анимация теперь управляется AnimatedKeyboardTransition + EmojiPickerContent( + isDarkTheme = isDarkTheme, + onEmojiSelected = onEmojiSelected, + keyboardHeight = savedKeyboardHeight, modifier = modifier - ) { - // 🎨 Hardware layer для анимаций (GPU ускорение - как в Telegram) - Box( - modifier = Modifier.graphicsLayer { - // Используем hardware layer только во время анимации - if (transition.isRunning) { - this.alpha = 1f - } - } - ) { - EmojiPickerContent( - isDarkTheme = isDarkTheme, - onEmojiSelected = onEmojiSelected, - keyboardHeight = savedKeyboardHeight - ) - } - } + ) } /** @@ -130,7 +90,8 @@ fun OptimizedEmojiPicker( private fun EmojiPickerContent( isDarkTheme: Boolean, onEmojiSelected: (String) -> Unit, - keyboardHeight: Dp + keyboardHeight: Dp, + modifier: Modifier = Modifier ) { val context = LocalContext.current var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) } @@ -185,7 +146,7 @@ private fun EmojiPickerContent( val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f) Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .height(height = keyboardHeight) // 🔥 Используем сохранённую высоту (как в Telegram) .background(panelBackground)