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 5989f6c..9b240b0 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,33 +1,31 @@ package app.rosette.android.ui.keyboard import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState 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.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -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 /** - * 🚀 МЕТОД: Animated Height + Content Offset (Telegram-style) + * 🚀 Telegram-style: Fixed Height Box + Fade In/Out * - * Ключевые принципы: - * 1. ВЫСОТА контейнера анимируется от 0dp до emojiHeight - * 2. Контент внутри clipToBounds - обрезается по границам - * 3. НЕТ предварительного резервирования места когда скрыт - * 4. graphicsLayer для GPU-ускорения + * Принцип как в Telegram: + * 1. Box ВСЕГДА существует когда нужен (showEmojiPicker или анимация идёт) + * 2. Box сразу занимает место = emojiHeight + * 3. Контент появляется через fade (alpha animation) + * 4. Клавиатура уезжает, а эмодзи уже на месте * - * Паттерн: - * - Показать: height анимируется от 0dp до emojiHeight (контент появляется снизу) - * - Скрыть: height анимируется от emojiHeight до 0dp (контент уходит вниз) - * - Длительность: 250ms с FastOutSlowInEasing + * Ключевое: Box показан когда showEmojiPicker ИЛИ alpha > 0 (анимация ещё идёт) */ @Composable fun AnimatedKeyboardTransition( @@ -37,59 +35,65 @@ fun AnimatedKeyboardTransition( ) { val tag = "AnimatedTransition" - // 🎯 Целевая высота: emojiHeight = видно, 0dp = скрыто - val targetHeight = if (showEmojiPicker) { - coordinator.emojiHeight - } else { - 0.dp + // 🔥 Отслеживаем был ли emoji открыт (для определения перехода emoji→keyboard) + val wasEmojiShown = remember { mutableStateOf(false) } + val isTransitioningToKeyboard = remember { mutableStateOf(false) } + + // Когда emoji открывается - запоминаем + LaunchedEffect(showEmojiPicker) { + if (showEmojiPicker) { + wasEmojiShown.value = true + } else if (wasEmojiShown.value) { + // Emoji закрылся после того как был открыт = переход emoji→keyboard + isTransitioningToKeyboard.value = true + } } - // 🎬 Декларативная анимация высоты - val animatedHeight by animateDpAsState( - targetValue = targetHeight, + // Сбрасываем флаг перехода когда клавиатура достигла полной высоты + val isKeyboardFullHeight = coordinator.keyboardHeight >= coordinator.emojiHeight * 0.95f && coordinator.emojiHeight > 0.dp + LaunchedEffect(isKeyboardFullHeight) { + if (isKeyboardFullHeight && isTransitioningToKeyboard.value) { + isTransitioningToKeyboard.value = false + wasEmojiShown.value = false + } + } + + // 🎯 Целевая прозрачность + val targetAlpha = if (showEmojiPicker) 1f else 0f + + // 🎬 Анимация прозрачности + val animatedAlpha by animateFloatAsState( + targetValue = targetAlpha, animationSpec = tween( durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(), easing = FastOutSlowInEasing ), - label = "emojiPanelHeight" + label = "emojiPanelAlpha" ) - Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, targetH=$targetHeight, currentH=$animatedHeight, emojiH=${coordinator.emojiHeight}") + // 🔥 Box показан когда: + // 1. showEmojiPicker=true (emoji открыт), ИЛИ + // 2. анимация fade-out ещё идёт (alpha > 0.01), ИЛИ + // 3. переход emoji→keyboard И клавиатура ещё не достигла полной высоты + val isFadingOut = !showEmojiPicker && animatedAlpha > 0.01f + val shouldShowBox = showEmojiPicker || isFadingOut || isTransitioningToKeyboard.value - // 🔥 Рендерим только если есть высота (показан или анимируется) - if (animatedHeight > 0.dp) { + // 🔥 ВАЖНО: Обновляем состояние в координаторе для отключения imePadding + coordinator.isEmojiBoxVisible = shouldShowBox + + Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, alpha=$animatedAlpha, shouldShow=$shouldShowBox, transitioning=${isTransitioningToKeyboard.value}, kbHeight=${coordinator.keyboardHeight}") + + if (shouldShowBox) { Box( modifier = Modifier .fillMaxWidth() - .height(animatedHeight) - .clipToBounds() // 🔥 Обрезаем контент по границам контейнера - .graphicsLayer { - // GPU-ускорение для плавности - clip = true + .height(coordinator.emojiHeight) + .graphicsLayer { + // 🔥 GPU-ускоренная анимация прозрачности + alpha = animatedAlpha } ) { - // Внутренний контейнер с полной высотой 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() - } + 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 index dfd14be..f8b4139 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 @@ -64,12 +64,13 @@ class KeyboardTransitionCoordinator { var isEmojiVisible by mutableStateOf(false) var isTransitioning by mutableStateOf(false) private set + + // 🔥 Показывается ли сейчас Box с эмодзи (включая анимацию fade-out) + // Используется для отключения imePadding пока Box виден + var isEmojiBoxVisible by mutableStateOf(false) // 🔥 Коллбэк для показа emoji (сохраняем для вызова после закрытия клавиатуры) private var pendingShowEmojiCallback: (() -> Unit)? = null - - // ============ Главный метод: Keyboard → Emoji ============ - /** * Переход от системной клавиатуры к emoji панели. * 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 24af007..c9feab4 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 @@ -905,10 +905,18 @@ fun ChatDetailScreen( } // Закрытие AnimatedVisibility для normal header }, containerColor = backgroundColor, // Фон всего чата - // 🔥 Bottom bar - инпут с imePadding автоматически поднимается над клавиатурой + // 🔥 Bottom bar - инпут с умным padding: + // - Когда showEmojiPicker=false → imePadding (поднимается над клавиатурой) + // - Когда showEmojiPicker=true → НЕТ imePadding (Box с эмодзи сам даёт высоту) bottomBar = { - // 🔥 Весь bottomBar поднимается над клавиатурой - Column(modifier = Modifier.imePadding()) { + // 🔥 Telegram-style: когда Box с эмодзи виден, НЕ используем imePadding + // isEmojiBoxVisible учитывает анимацию fade-out (alpha > 0.01) + val bottomModifier = if (coordinator.isEmojiBoxVisible) { + Modifier // Без imePadding - Box с эмодзи заменяет клавиатуру + } else { + Modifier.imePadding() // С imePadding - клавиатура поднимает инпут + } + Column(modifier = bottomModifier) { // 🔥 FLOATING INPUT BAR - плавает поверх сообщений, поднимается с клавиатурой // Скрываем когда в режиме выбора AnimatedVisibility(