From 5c5c5e45ee488329c4038711cdff1d84c90c1ede Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 3 Feb 2026 00:18:49 +0500 Subject: [PATCH] fix: improve emoji picker and keyboard transition animations for smoother user experience --- .../ui/chats/components/ImageEditorScreen.kt | 125 ++++++++++++------ 1 file changed, 87 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt index 90229df..36584a1 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt @@ -528,17 +528,52 @@ fun ImageEditorScreen( // 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji НЕ показан val shouldUseImePadding = !coordinator.isEmojiBoxVisible + + // 🔥 FIX: Сырое значение - emoji показан ИЛИ box виден + val rawEmojiVisible = showEmojiPicker || coordinator.isEmojiBoxVisible + + // 🔥 FIX: Debounced флаг для emoji - с задержкой при закрытии чтобы не мигал + var stableShowEmojiPicker by remember { mutableStateOf(rawEmojiVisible) } + + LaunchedEffect(rawEmojiVisible) { + if (rawEmojiVisible) { + // Открытие emoji - мгновенно + stableShowEmojiPicker = true + } else { + // Закрытие emoji - с небольшой задержкой для плавности + delay(50) + stableShowEmojiPicker = false + } + } + + // 🔥 FIX: Debounced флаг для стилей инпута - предотвращает мигание при закрытии emoji + // Истинный когда клавиатура ИЛИ emoji открыты + val isInputExpanded = isKeyboardVisible || stableShowEmojiPicker + + // 🔥 FIX: Инициализируем с актуальным значением чтобы не было мигания при первом рендере + var isInputExpandedDebounced by remember { mutableStateOf(isInputExpanded) } + + LaunchedEffect(isInputExpanded) { + if (isInputExpanded) { + // Открытие - мгновенно + isInputExpandedDebounced = true + } else { + // Закрытие - с задержкой чтобы анимация emoji завершилась + delay(300) + isInputExpandedDebounced = false + } + } // Когда клавиатура/emoji закрыты - добавляем отступ снизу для toolbar (~100dp) // 🔥 Анимируем отступ для плавного перехода val bottomPaddingForCaption by animateDpAsState( - targetValue = if (!isKeyboardVisible && !coordinator.isEmojiBoxVisible) 100.dp else 0.dp, + targetValue = if (!isInputExpandedDebounced) 100.dp else 0.dp, animationSpec = tween(200, easing = FastOutSlowInEasing), label = "bottomPadding" ) // 📊 Log render state - Log.d(TAG, "RENDER: showEmoji=$showEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp, bottomPadding=${bottomPaddingForCaption.value}dp") + Log.d(TAG, "RENDER: showEmoji=$showEmojiPicker, stableEmoji=$stableShowEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp, bottomPadding=${bottomPaddingForCaption.value}dp") AnimatedVisibility( visible = showCaptionInput && currentTool == EditorTool.NONE, @@ -551,17 +586,19 @@ fun ImageEditorScreen( Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = bottomPaddingForCaption) - // 🔥 imePadding ТОЛЬКО когда emoji НЕ показан + // 🔥 imePadding СНАЧАЛА - поднимает над клавиатурой .then(if (shouldUseImePadding) Modifier.imePadding() else Modifier) + // 🔥 Затем добавляем отступ для toolbar когда клавиатура закрыта + .padding(bottom = bottomPaddingForCaption) ) { TelegramCaptionBar( caption = caption, onCaptionChange = { caption = it }, isSaving = isSaving, - // 🔥 Добавляем isEmojiBoxVisible чтобы во время перехода emoji→keyboard инпут не сжимался - isKeyboardVisible = isKeyboardVisible || showEmojiPicker || coordinator.isEmojiBoxVisible, - showEmojiPicker = showEmojiPicker, + // 🔥 Используем debounced флаг чтобы стили не мигали при закрытии emoji + isKeyboardVisible = isInputExpandedDebounced, + // 🔥 FIX: Используем стабильный флаг чтобы иконка не мигала + showEmojiPicker = stableShowEmojiPicker, onToggleEmojiPicker = { toggleEmojiPicker() }, onEditTextViewCreated = { editTextView = it }, onSend = { @@ -979,34 +1016,40 @@ private fun TelegramCaptionBar( onEditTextViewCreated: ((com.rosetta.messenger.ui.components.AppleEmojiEditTextView) -> Unit)? = null, onSend: () -> Unit ) { - // Анимированный переход между стилями + // � FIX: Анимируем ВСЕ параметры стиля для плавного перехода без мигания val cornerRadius by animateDpAsState( targetValue = if (isKeyboardVisible) 0.dp else 24.dp, - animationSpec = tween(200, easing = TelegramEasing), + animationSpec = tween(250, easing = FastOutSlowInEasing), label = "corner" ) val horizontalPadding by animateDpAsState( targetValue = if (isKeyboardVisible) 0.dp else 12.dp, - animationSpec = tween(200, easing = TelegramEasing), + animationSpec = tween(250, easing = FastOutSlowInEasing), label = "hPadding" ) + + // 🔥 FIX: Анимируем прозрачность и цвет фона для плавного перехода + val backgroundAlpha by animateFloatAsState( + targetValue = if (isKeyboardVisible) 0.75f else 0.85f, + animationSpec = tween(250, easing = FastOutSlowInEasing), + label = "bgAlpha" + ) + + // 🔥 FIX: Плавный переход цвета фона + val backgroundColor by animateColorAsState( + targetValue = if (isKeyboardVisible) Color.Black else Color(0xFF2C2C2E), + animationSpec = tween(250, easing = FastOutSlowInEasing), + label = "bgColor" + ) Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = horizontalPadding) - .then( - if (isKeyboardVisible) { - // Клавиатура/emoji открыты - полупрозрачный черный фон на всю ширину - Modifier.background(Color.Black.copy(alpha = 0.75f)) - } else { - // Клавиатура закрыта - стеклянный эффект с закруглением - Modifier - .clip(RoundedCornerShape(cornerRadius)) - .background(Color(0xFF2C2C2E).copy(alpha = 0.85f)) - } - ) + // 🔥 FIX: Единый фон с анимированными параметрами - без мигания! + .clip(RoundedCornerShape(cornerRadius)) + .background(backgroundColor.copy(alpha = backgroundAlpha)) .padding(horizontal = 12.dp, vertical = 10.dp) ) { Row( @@ -1015,11 +1058,10 @@ private fun TelegramCaptionBar( horizontalArrangement = Arrangement.spacedBy(10.dp) ) { // Левая иконка: камера когда клавиатура закрыта, emoji/keyboard когда открыта - AnimatedContent( + // 🔥 FIX: Crossfade вместо AnimatedContent для более плавной анимации + Crossfade( targetState = isKeyboardVisible to showEmojiPicker, - transitionSpec = { - fadeIn(tween(150)) togetherWith fadeOut(tween(150)) - }, + animationSpec = tween(200), label = "left_icon" ) { (keyboardOpen, emojiOpen) -> if (keyboardOpen) { @@ -1036,21 +1078,30 @@ private fun TelegramCaptionBar( ) } } else { - // Клавиатура закрыта - камера иконка - Icon( - TablerIcons.CameraPlus, - contentDescription = "Camera", - tint = Color.White.copy(alpha = 0.7f), - modifier = Modifier.size(26.dp) - ) + // Клавиатура закрыта - камера иконка (с одинаковым размером для избежания прыжка) + Box(modifier = Modifier.size(32.dp), contentAlignment = Alignment.Center) { + Icon( + TablerIcons.CameraPlus, + contentDescription = "Camera", + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(26.dp) + ) + } } } // Caption text field - использует AppleEmojiTextField для правильной работы с фокусом + // 🔥 FIX: Анимируем высоту текстового поля + val textFieldMaxHeight by animateDpAsState( + targetValue = if (isKeyboardVisible) 100.dp else 24.dp, + animationSpec = tween(200, easing = FastOutSlowInEasing), + label = "textFieldHeight" + ) + Box( modifier = Modifier .weight(1f) - .heightIn(min = 24.dp, max = if (isKeyboardVisible) 100.dp else 24.dp) + .heightIn(min = 24.dp, max = textFieldMaxHeight) ) { AppleEmojiTextField( value = caption, @@ -1071,12 +1122,10 @@ private fun TelegramCaptionBar( ) } - // Кнопка отправки - AnimatedContent( + // 🔥 FIX: Crossfade вместо AnimatedContent для кнопки отправки + Crossfade( targetState = isKeyboardVisible, - transitionSpec = { - fadeIn(tween(150)) togetherWith fadeOut(tween(150)) - }, + animationSpec = tween(200), label = "send_button" ) { keyboardOpen -> Box(