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 package app.rosette.android.ui.keyboard
import androidx.compose.animation.core.FastOutSlowInEasing 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.animation.core.tween
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import android.util.Log import android.util.Log
/** /**
* 🚀 МЕТОД: Animated Height + Content Offset (Telegram-style) * 🚀 Telegram-style: Fixed Height Box + Fade In/Out
* *
* Ключевые принципы: * Принцип как в Telegram:
* 1. ВЫСОТА контейнера анимируется от 0dp до emojiHeight * 1. Box ВСЕГДА существует когда нужен (showEmojiPicker или анимация идёт)
* 2. Контент внутри clipToBounds - обрезается по границам * 2. Box сразу занимает место = emojiHeight
* 3. НЕТ предварительного резервирования места когда скрыт * 3. Контент появляется через fade (alpha animation)
* 4. graphicsLayer для GPU-ускорения * 4. Клавиатура уезжает, а эмодзи уже на месте
* *
* Паттерн: * Ключевое: Box показан когда showEmojiPicker ИЛИ alpha > 0 (анимация ещё идёт)
* - Показать: height анимируется от 0dp до emojiHeight (контент появляется снизу)
* - Скрыть: height анимируется от emojiHeight до 0dp (контент уходит вниз)
* - Длительность: 250ms с FastOutSlowInEasing
*/ */
@Composable @Composable
fun AnimatedKeyboardTransition( fun AnimatedKeyboardTransition(
@@ -37,59 +35,65 @@ fun AnimatedKeyboardTransition(
) { ) {
val tag = "AnimatedTransition" val tag = "AnimatedTransition"
// 🎯 Целевая высота: emojiHeight = видно, 0dp = скрыто // 🔥 Отслеживаем был ли emoji открыт (для определения перехода emoji→keyboard)
val targetHeight = if (showEmojiPicker) { val wasEmojiShown = remember { mutableStateOf(false) }
coordinator.emojiHeight val isTransitioningToKeyboard = remember { mutableStateOf(false) }
} else {
0.dp // Когда emoji открывается - запоминаем
LaunchedEffect(showEmojiPicker) {
if (showEmojiPicker) {
wasEmojiShown.value = true
} else if (wasEmojiShown.value) {
// Emoji закрылся после того как был открыт = переход emoji→keyboard
isTransitioningToKeyboard.value = true
}
} }
// 🎬 Декларативная анимация высоты // Сбрасываем флаг перехода когда клавиатура достигла полной высоты
val animatedHeight by animateDpAsState( val isKeyboardFullHeight = coordinator.keyboardHeight >= coordinator.emojiHeight * 0.95f && coordinator.emojiHeight > 0.dp
targetValue = targetHeight, 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( animationSpec = tween(
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(), durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
easing = FastOutSlowInEasing 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
// 🔥 Рендерим только если есть высота (показан или анимируется) // 🔥 ВАЖНО: Обновляем состояние в координаторе для отключения imePadding
if (animatedHeight > 0.dp) { coordinator.isEmojiBoxVisible = shouldShowBox
Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, alpha=$animatedAlpha, shouldShow=$shouldShowBox, transitioning=${isTransitioningToKeyboard.value}, kbHeight=${coordinator.keyboardHeight}")
if (shouldShowBox) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(animatedHeight) .height(coordinator.emojiHeight)
.clipToBounds() // 🔥 Обрезаем контент по границам контейнера .graphicsLayer {
.graphicsLayer { // 🔥 GPU-ускоренная анимация прозрачности
// GPU-ускорение для плавности alpha = animatedAlpha
clip = true
} }
) { ) {
// Внутренний контейнер с полной высотой emoji content()
// Он обрезается 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()
}
} }
} }
} }

View File

@@ -64,12 +64,13 @@ class KeyboardTransitionCoordinator {
var isEmojiVisible by mutableStateOf(false) var isEmojiVisible by mutableStateOf(false)
var isTransitioning by mutableStateOf(false) var isTransitioning by mutableStateOf(false)
private set private set
// 🔥 Показывается ли сейчас Box с эмодзи (включая анимацию fade-out)
// Используется для отключения imePadding пока Box виден
var isEmojiBoxVisible by mutableStateOf(false)
// 🔥 Коллбэк для показа emoji (сохраняем для вызова после закрытия клавиатуры) // 🔥 Коллбэк для показа emoji (сохраняем для вызова после закрытия клавиатуры)
private var pendingShowEmojiCallback: (() -> Unit)? = null private var pendingShowEmojiCallback: (() -> Unit)? = null
// ============ Главный метод: Keyboard → Emoji ============
/** /**
* Переход от системной клавиатуры к emoji панели. * Переход от системной клавиатуры к emoji панели.
* *

View File

@@ -905,10 +905,18 @@ fun ChatDetailScreen(
} // Закрытие AnimatedVisibility для normal header } // Закрытие AnimatedVisibility для normal header
}, },
containerColor = backgroundColor, // Фон всего чата containerColor = backgroundColor, // Фон всего чата
// 🔥 Bottom bar - инпут с imePadding автоматически поднимается над клавиатурой // 🔥 Bottom bar - инпут с умным padding:
// - Когда showEmojiPicker=false → imePadding (поднимается над клавиатурой)
// - Когда showEmojiPicker=true → НЕТ imePadding (Box с эмодзи сам даёт высоту)
bottomBar = { bottomBar = {
// 🔥 Весь bottomBar поднимается над клавиатурой // 🔥 Telegram-style: когда Box с эмодзи виден, НЕ используем imePadding
Column(modifier = Modifier.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 - плавает поверх сообщений, поднимается с клавиатурой // 🔥 FLOATING INPUT BAR - плавает поверх сообщений, поднимается с клавиатурой
// Скрываем когда в режиме выбора // Скрываем когда в режиме выбора
AnimatedVisibility( AnimatedVisibility(