feat: Enhance emoji picker transition with fade animations and optimize imePadding behavior
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 панели.
|
||||
*
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user