feat: Enhance emoji picker transition with fade animations and optimize imePadding behavior

This commit is contained in:
k1ngsterr1
2026-01-15 14:18:48 +05:00
parent 911f9ebb5a
commit ed4622ae27
3 changed files with 71 additions and 58 deletions

View File

@@ -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,61 +35,67 @@ 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) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(animatedHeight)
.clipToBounds() // 🔥 Обрезаем контент по границам контейнера
.graphicsLayer {
// GPU-ускорение для плавности
clip = true
}
) {
// Внутренний контейнер с полной высотой emoji
// Он обрезается clipToBounds родителя
// 🔥 ВАЖНО: Обновляем состояние в координаторе для отключения 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(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)
}
.graphicsLayer {
// 🔥 GPU-ускоренная анимация прозрачности
alpha = animatedAlpha
}
) {
content()
}
}
}
}
/**

View File

@@ -65,11 +65,12 @@ class KeyboardTransitionCoordinator {
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 панели.
*

View File

@@ -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(