feat: Implement smooth keyboard transition plan with Telegram-style animations
- Add KeyboardTransitionCoordinator for managing transitions between keyboard and emoji panel. - Create AnimatedKeyboardTransition for handling emoji panel animations with slide and fade effects. - Integrate keyboard transition logic into MessageInputBar for seamless emoji picker toggling. - Update OptimizedEmojiPicker to utilize external animation management instead of internal visibility animations. - Ensure synchronization of keyboard and emoji heights for consistent UI behavior.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package app.rosette.android.ui.keyboard
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
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.foundation.layout.offset
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Анимированный контейнер для emoji панели с Telegram-style переходами.
|
||||
*
|
||||
* Паттерны Telegram:
|
||||
* - Keyboard → Emoji: панель выезжает снизу за 250ms (FastOutSlowIn)
|
||||
* - Emoji → Keyboard: панель уезжает вниз за 200ms (FastOutLinearIn)
|
||||
* - TranslationY + Alpha для плавности
|
||||
* - Немедленное резервирование места
|
||||
*/
|
||||
@Composable
|
||||
fun AnimatedKeyboardTransition(
|
||||
coordinator: KeyboardTransitionCoordinator,
|
||||
showEmojiPicker: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val tag = "AnimatedTransition"
|
||||
|
||||
// Animatable для плавной анимации offset
|
||||
val scope = rememberCoroutineScope()
|
||||
val offsetY = remember { Animatable(0f) }
|
||||
val alpha = remember { Animatable(1f) }
|
||||
|
||||
// Отслеживаем изменения showEmojiPicker
|
||||
LaunchedEffect(showEmojiPicker) {
|
||||
Log.d(tag, "════════════════════════════════════════════════════════")
|
||||
Log.d(tag, "🎬 Animation triggered: showEmojiPicker=$showEmojiPicker")
|
||||
Log.d(tag, "📊 Current state: offsetY=${offsetY.value}dp, alpha=${alpha.value}, emojiHeight=${coordinator.emojiHeight.value}dp")
|
||||
|
||||
if (showEmojiPicker) {
|
||||
// ============ ПОКАЗАТЬ EMOJI ============
|
||||
|
||||
val height = coordinator.emojiHeight.value
|
||||
Log.d(tag, "📤 Animating emoji IN from ${height}dp to 0dp")
|
||||
Log.d(tag, "⏱️ Duration: ${KeyboardTransitionCoordinator.TRANSITION_DURATION}ms, Easing: FastOutSlowIn")
|
||||
|
||||
// Начальная позиция: внизу за экраном
|
||||
Log.d(tag, "🎯 Setting initial offset to ${height}dp (off-screen)")
|
||||
offsetY.snapTo(height)
|
||||
scope.launch {
|
||||
offsetY.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
Log.d(tag, "✅ Emoji slide animation completed")
|
||||
}
|
||||
|
||||
// Fade in (немного быстрее)
|
||||
Log.d(tag, "✨ Starting fade in animation (200ms)")
|
||||
scope.launch {
|
||||
alpha.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(durationMillis = 200)
|
||||
)
|
||||
Log.d(tag, "✅ Fade in completed, alpha=${alpha.value}")
|
||||
}
|
||||
|
||||
} else if (offsetY.value < coordinator.emojiHeight.value * 0.9f) {
|
||||
// ============ СКРЫТЬ EMOJI ============
|
||||
Log.d(tag, "📥 Hiding emoji panel (offsetY=${offsetY.value}dp < threshold=${coordinator.emojiHeight.value * 0.9f}dp)")
|
||||
|
||||
val height = coordinator.emojiHeight.value
|
||||
Log.d(tag, "🎯 Animating emoji OUT from ${offsetY.value}dp to ${height}dp")
|
||||
Log.d(tag, "⏱️ Duration: 200ms, Easing: FastOutLinearIn")
|
||||
scope.launch {
|
||||
offsetY.animateTo(
|
||||
targetValue = height,
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
Log.d(tag, "✅ Emoji hide animation completed")
|
||||
}
|
||||
|
||||
// Fade out (быстрее)
|
||||
Log.d(tag, "🌑 Starting fade out animation (150ms)")
|
||||
scope.launch {
|
||||
alpha.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(durationMillis = 150)
|
||||
)
|
||||
Log.d(tag, "✅ Fade out completed, alpha=${alpha.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 Рендерим контент ТОЛЬКО если showEmojiPicker = true
|
||||
// Не проверяем offsetY, чтобы избежать показа emoji при открытии клавиатуры
|
||||
if (showEmojiPicker) {
|
||||
Log.d(tag, "✅ Rendering emoji content (showEmojiPicker=true)")
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(coordinator.emojiHeight)
|
||||
.offset(y = offsetY.value.dp)
|
||||
.alpha(alpha.value)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Упрощенная версия без fade анимации.
|
||||
* Только slide (как в оригинальном Telegram).
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleAnimatedKeyboardTransition(
|
||||
coordinator: KeyboardTransitionCoordinator,
|
||||
showEmojiPicker: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val offsetY = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(showEmojiPicker) {
|
||||
if (showEmojiPicker) {
|
||||
// Показать: снизу вверх
|
||||
offsetY.snapTo(coordinator.emojiHeight.value)
|
||||
offsetY.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Скрыть: сверху вниз
|
||||
offsetY.animateTo(
|
||||
targetValue = coordinator.emojiHeight.value,
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showEmojiPicker || offsetY.value < coordinator.emojiHeight.value * 0.95f) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(coordinator.emojiHeight)
|
||||
.offset(y = offsetY.value.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package app.rosette.android.ui.keyboard
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Координатор переходов между системной клавиатурой и emoji панелью.
|
||||
* Реализует Telegram-style плавные анимации.
|
||||
*
|
||||
* Ключевые принципы:
|
||||
* - 250ms duration (как в Telegram AdjustPanLayoutHelper)
|
||||
* - Немедленное резервирование места
|
||||
* - TranslationY анимации вместо show/hide
|
||||
* - Синхронизация всех переходов
|
||||
*/
|
||||
class KeyboardTransitionCoordinator {
|
||||
|
||||
companion object {
|
||||
const val TRANSITION_DURATION = 250L
|
||||
const val SHORT_DELAY = 50L
|
||||
private const val TAG = "KeyboardTransition"
|
||||
}
|
||||
|
||||
// ============ Состояния переходов ============
|
||||
|
||||
enum class TransitionState {
|
||||
IDLE, // Ничего не происходит
|
||||
KEYBOARD_TO_EMOJI, // Keyboard → Emoji
|
||||
EMOJI_TO_KEYBOARD, // Emoji → Keyboard
|
||||
KEYBOARD_OPENING, // Только keyboard открывается
|
||||
EMOJI_OPENING, // Только emoji открывается
|
||||
KEYBOARD_CLOSING, // Только keyboard закрывается
|
||||
EMOJI_CLOSING // Только emoji закрывается
|
||||
}
|
||||
|
||||
// ============ Основное состояние ============
|
||||
|
||||
var currentState by mutableStateOf(TransitionState.IDLE)
|
||||
private set
|
||||
|
||||
var transitionProgress by mutableFloatStateOf(0f)
|
||||
private set
|
||||
|
||||
// ============ Высоты ============
|
||||
|
||||
var keyboardHeight by mutableStateOf(0.dp)
|
||||
var emojiHeight by mutableStateOf(0.dp)
|
||||
|
||||
// ============ Флаги видимости ============
|
||||
|
||||
var isKeyboardVisible by mutableStateOf(false)
|
||||
var isEmojiVisible by mutableStateOf(false)
|
||||
var isTransitioning by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
// 🔥 Коллбэк для показа emoji (сохраняем для вызова после закрытия клавиатуры)
|
||||
private var pendingShowEmojiCallback: (() -> Unit)? = null
|
||||
|
||||
// ============ Главный метод: Keyboard → Emoji ============
|
||||
|
||||
/**
|
||||
* Переход от системной клавиатуры к emoji панели.
|
||||
* Telegram паттерн: сначала скрыть клавиатуру, затем показать emoji.
|
||||
*/
|
||||
fun requestShowEmoji(
|
||||
hideKeyboard: () -> Unit,
|
||||
showEmoji: () -> Unit
|
||||
) {
|
||||
Log.d(TAG, "═══════════════════════════════════════════════════════")
|
||||
Log.d(TAG, "📱 requestShowEmoji() START")
|
||||
Log.d(TAG, "🔄 Keyboard → Emoji transition started")
|
||||
Log.d(TAG, " 📊 Current state:")
|
||||
Log.d(TAG, " - currentState=$currentState")
|
||||
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
|
||||
Log.d(TAG, " - isTransitioning=$isTransitioning")
|
||||
Log.d(TAG, " - isKeyboardVisible=$isKeyboardVisible, isEmojiVisible=$isEmojiVisible")
|
||||
Log.d(TAG, " - pendingShowEmojiCallback=${if (pendingShowEmojiCallback != null) "EXISTS" else "null"}")
|
||||
|
||||
Log.d(TAG, " 📝 Setting currentState = KEYBOARD_TO_EMOJI")
|
||||
currentState = TransitionState.KEYBOARD_TO_EMOJI
|
||||
isTransitioning = true
|
||||
|
||||
// 🔥 Сохраняем коллбэк для вызова после закрытия клавиатуры
|
||||
Log.d(TAG, " 💾 Saving pending emoji callback...")
|
||||
pendingShowEmojiCallback = {
|
||||
try {
|
||||
Log.d(TAG, "🎬 Executing pending emoji show callback")
|
||||
Log.d(TAG, " Current keyboard height at execution: $keyboardHeight")
|
||||
Log.d(TAG, " 📞 Calling showEmoji()...")
|
||||
showEmoji()
|
||||
Log.d(TAG, " 📞 Setting isEmojiVisible = true")
|
||||
isEmojiVisible = true
|
||||
isKeyboardVisible = false
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
pendingShowEmojiCallback = null
|
||||
Log.d(TAG, "✅ Emoji shown via pending callback")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error in pendingShowEmojiCallback", e)
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
pendingShowEmojiCallback = null
|
||||
}
|
||||
}
|
||||
Log.d(TAG, " ✅ Pending callback saved")
|
||||
|
||||
// Шаг 1: Скрыть системную клавиатуру
|
||||
Log.d(TAG, " 📞 Calling hideKeyboard()...")
|
||||
try {
|
||||
hideKeyboard()
|
||||
Log.d(TAG, " ✅ hideKeyboard() completed")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error hiding keyboard", e)
|
||||
}
|
||||
|
||||
// Если клавиатура уже закрыта (height = 0), показываем emoji сразу
|
||||
if (keyboardHeight <= 0.dp) {
|
||||
Log.d(TAG, "⚡ Keyboard already closed, showing emoji immediately")
|
||||
pendingShowEmojiCallback?.invoke()
|
||||
}
|
||||
// Иначе ждем когда updateKeyboardHeight получит 0
|
||||
}
|
||||
|
||||
// ============ Главный метод: Emoji → Keyboard ============
|
||||
|
||||
/**
|
||||
* Переход от emoji панели к системной клавиатуре.
|
||||
* Telegram паттерн: показать клавиатуру и плавно скрыть emoji.
|
||||
*/
|
||||
fun requestShowKeyboard(
|
||||
showKeyboard: () -> Unit,
|
||||
hideEmoji: () -> Unit
|
||||
) {
|
||||
Log.d(TAG, "═══════════════════════════════════════════════════════")
|
||||
Log.d(TAG, "⌨️ requestShowKeyboard() START")
|
||||
Log.d(TAG, "🔄 Emoji → Keyboard transition started")
|
||||
Log.d(TAG, " 📊 Current state:")
|
||||
Log.d(TAG, " - currentState=$currentState")
|
||||
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
|
||||
Log.d(TAG, " - isTransitioning=$isTransitioning")
|
||||
Log.d(TAG, " - isKeyboardVisible=$isKeyboardVisible, isEmojiVisible=$isEmojiVisible")
|
||||
Log.d(TAG, " - pendingShowEmojiCallback=${if (pendingShowEmojiCallback != null) "EXISTS" else "null"}")
|
||||
|
||||
// 🔥 Отменяем pending emoji callback если он есть (предотвращаем конфликт)
|
||||
if (pendingShowEmojiCallback != null) {
|
||||
Log.d(TAG, "⚠️ Cancelling pending emoji callback (switching to keyboard)")
|
||||
pendingShowEmojiCallback = null
|
||||
}
|
||||
|
||||
Log.d(TAG, " 📞 Setting currentState = EMOJI_TO_KEYBOARD")
|
||||
currentState = TransitionState.EMOJI_TO_KEYBOARD
|
||||
isTransitioning = true
|
||||
|
||||
// Шаг 1: Показать системную клавиатуру
|
||||
Log.d(TAG, " 📞 Calling showKeyboard()...")
|
||||
try {
|
||||
showKeyboard()
|
||||
Log.d(TAG, " ✅ showKeyboard() completed")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error showing keyboard", e)
|
||||
}
|
||||
|
||||
// Шаг 2: Через небольшую задержку скрыть emoji
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
try {
|
||||
hideEmoji()
|
||||
isEmojiVisible = false
|
||||
isKeyboardVisible = true
|
||||
|
||||
// Через время анимации завершаем переход
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
Log.d(TAG, "✅ Keyboard visible, emoji hidden")
|
||||
}, TRANSITION_DURATION)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error in requestShowKeyboard transition", e)
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
}
|
||||
}, SHORT_DELAY)
|
||||
}
|
||||
|
||||
// ============ Простые переходы ============
|
||||
|
||||
/**
|
||||
* Открыть только emoji панель (без клавиатуры).
|
||||
*/
|
||||
fun openEmojiOnly(showEmoji: () -> Unit) {
|
||||
Log.d(TAG, "😊 Opening emoji panel only")
|
||||
|
||||
currentState = TransitionState.EMOJI_OPENING
|
||||
isTransitioning = true
|
||||
|
||||
// Установить высоту emoji равной сохраненной высоте клавиатуры
|
||||
if (emojiHeight == 0.dp && keyboardHeight > 0.dp) {
|
||||
emojiHeight = keyboardHeight
|
||||
}
|
||||
|
||||
showEmoji()
|
||||
isEmojiVisible = true
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
Log.d(TAG, "✅ Emoji panel opened")
|
||||
}, TRANSITION_DURATION)
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть emoji панель.
|
||||
*/
|
||||
fun closeEmoji(hideEmoji: () -> Unit) {
|
||||
Log.d(TAG, "😊 Closing emoji panel")
|
||||
|
||||
currentState = TransitionState.EMOJI_CLOSING
|
||||
isTransitioning = true
|
||||
|
||||
hideEmoji()
|
||||
isEmojiVisible = false
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
Log.d(TAG, "✅ Emoji panel closed")
|
||||
}, TRANSITION_DURATION)
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть системную клавиатуру.
|
||||
*/
|
||||
fun closeKeyboard(hideKeyboard: () -> Unit) {
|
||||
Log.d(TAG, "⌨️ Closing keyboard")
|
||||
|
||||
currentState = TransitionState.KEYBOARD_CLOSING
|
||||
isTransitioning = true
|
||||
|
||||
hideKeyboard()
|
||||
isKeyboardVisible = false
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
Log.d(TAG, "✅ Keyboard closed")
|
||||
}, TRANSITION_DURATION)
|
||||
}
|
||||
|
||||
// ============ Вспомогательные методы ============
|
||||
|
||||
/**
|
||||
* Обновить высоту клавиатуры из IME.
|
||||
*/
|
||||
fun updateKeyboardHeight(height: Dp) {
|
||||
Log.d(TAG, "════════════════════════════════════════════════════════")
|
||||
Log.d(TAG, "📏 updateKeyboardHeight called: $keyboardHeight → $height")
|
||||
Log.d(TAG, " isKeyboardVisible=$isKeyboardVisible, emojiHeight=$emojiHeight")
|
||||
|
||||
if (height > 100.dp && height != keyboardHeight) {
|
||||
Log.d(TAG, "✅ Keyboard height updated: $keyboardHeight → $height")
|
||||
keyboardHeight = height
|
||||
|
||||
// Если emoji высота не установлена, синхронизировать
|
||||
if (emojiHeight == 0.dp) {
|
||||
Log.d(TAG, "🔄 Syncing emoji height (was 0): $emojiHeight → $height")
|
||||
emojiHeight = height
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "⏭️ No update needed (height too small or unchanged)")
|
||||
}
|
||||
|
||||
// 🔥 Если клавиатура закрылась И есть pending коллбэк → показываем emoji
|
||||
if (height == 0.dp && pendingShowEmojiCallback != null) {
|
||||
Log.d(TAG, "🎯 Keyboard closed (0dp), triggering pending emoji callback")
|
||||
val callback = pendingShowEmojiCallback
|
||||
pendingShowEmojiCallback = null // 🔥 Сразу обнуляем чтобы не вызвать дважды
|
||||
|
||||
// Вызываем через небольшую задержку чтобы UI стабилизировался
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
try {
|
||||
callback?.invoke()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error invoking pending emoji callback", e)
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить высоту emoji панели.
|
||||
*/
|
||||
fun updateEmojiHeight(height: Dp) {
|
||||
if (height > 0.dp && height != emojiHeight) {
|
||||
Log.d(TAG, "📏 Emoji height updated: $emojiHeight → $height")
|
||||
emojiHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Синхронизировать высоты (emoji = keyboard).
|
||||
*/
|
||||
fun syncHeights() {
|
||||
Log.d(TAG, "🔄 syncHeights called: keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
|
||||
if (keyboardHeight > 100.dp) {
|
||||
val oldEmojiHeight = emojiHeight
|
||||
emojiHeight = keyboardHeight
|
||||
Log.d(TAG, "✅ Heights synced: $oldEmojiHeight → $keyboardHeight")
|
||||
} else {
|
||||
Log.d(TAG, "⏭️ Keyboard height too small ($keyboardHeight), skipping sync")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить текущую высоту для резервирования места.
|
||||
* Telegram паттерн: всегда резервировать максимум из двух.
|
||||
*/
|
||||
fun getReservedHeight(): Dp {
|
||||
return when {
|
||||
isKeyboardVisible -> keyboardHeight
|
||||
isEmojiVisible -> emojiHeight
|
||||
isTransitioning -> maxOf(keyboardHeight, emojiHeight)
|
||||
else -> 0.dp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, можно ли начать новый переход.
|
||||
*/
|
||||
fun canStartTransition(): Boolean {
|
||||
return !isTransitioning
|
||||
}
|
||||
|
||||
/**
|
||||
* Сброс состояния (для отладки).
|
||||
*/
|
||||
fun reset() {
|
||||
Log.d(TAG, "🔄 Reset coordinator state")
|
||||
currentState = TransitionState.IDLE
|
||||
isTransitioning = false
|
||||
isKeyboardVisible = false
|
||||
isEmojiVisible = false
|
||||
transitionProgress = 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Логирование текущего состояния.
|
||||
*/
|
||||
fun logState() {
|
||||
Log.d(TAG, """
|
||||
📊 Coordinator State:
|
||||
- state: $currentState
|
||||
- transitioning: $isTransitioning
|
||||
- keyboardVisible: $isKeyboardVisible (height=$keyboardHeight)
|
||||
- emojiVisible: $isEmojiVisible (height=$emojiHeight)
|
||||
- progress: $transitionProgress
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable для создания и запоминания coordinator'а.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberKeyboardTransitionCoordinator(): KeyboardTransitionCoordinator {
|
||||
return remember { KeyboardTransitionCoordinator() }
|
||||
}
|
||||
@@ -70,6 +70,8 @@ import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||
import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
||||
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -1989,19 +1991,32 @@ private fun MessageInputBar(
|
||||
val view = LocalView.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
|
||||
val coordinator = rememberKeyboardTransitionCoordinator()
|
||||
|
||||
// 🔥 Ссылка на EditText для программного фокуса
|
||||
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
|
||||
|
||||
// 🔥 Автофокус при открытии reply панели
|
||||
LaunchedEffect(hasReply, editTextView) {
|
||||
if (hasReply) {
|
||||
android.util.Log.d("EmojiPicker", "═══════════════════════════════════════════════════════")
|
||||
android.util.Log.d("EmojiPicker", "💬 Reply panel opened, hasReply=$hasReply")
|
||||
android.util.Log.d("EmojiPicker", " 📊 editTextView=$editTextView, showEmojiPicker=$showEmojiPicker")
|
||||
// Даём время на создание view если ещё null
|
||||
kotlinx.coroutines.delay(50)
|
||||
editTextView?.let { editText ->
|
||||
editText.requestFocus()
|
||||
// Открываем клавиатуру
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
// 🔥 НЕ открываем клавиатуру если emoji уже открыт
|
||||
if (!showEmojiPicker) {
|
||||
android.util.Log.d("EmojiPicker", " ⌨️ Requesting focus and keyboard for reply...")
|
||||
editText.requestFocus()
|
||||
// Открываем клавиатуру
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
android.util.Log.d("EmojiPicker", " ✅ Auto-opened keyboard for reply")
|
||||
} else {
|
||||
android.util.Log.d("EmojiPicker", " ⏭️ Skip auto-keyboard for reply (emoji is open)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2014,9 +2029,24 @@ private fun MessageInputBar(
|
||||
// 🔥 Флаг "клавиатура в процессе анимации"
|
||||
var isKeyboardAnimating by remember { mutableStateOf(false) }
|
||||
|
||||
// 🔥 Логирование изменений высоты клавиатуры
|
||||
// 🔥 Логирование изменений высоты клавиатуры + обновление coordinator
|
||||
LaunchedEffect(imeHeight) {
|
||||
android.util.Log.d("KeyboardHeight", "📊 IME height: $imeHeight (visible=$isKeyboardVisible, showEmojiPicker=$showEmojiPicker, animating=$isKeyboardAnimating)")
|
||||
android.util.Log.d("KeyboardHeight", "═══════════════════════════════════════════════════════")
|
||||
android.util.Log.d("KeyboardHeight", "📊 IME height changed: $imeHeight")
|
||||
android.util.Log.d("KeyboardHeight", " isKeyboardVisible=$isKeyboardVisible")
|
||||
android.util.Log.d("KeyboardHeight", " showEmojiPicker=$showEmojiPicker")
|
||||
android.util.Log.d("KeyboardHeight", " isKeyboardAnimating=$isKeyboardAnimating")
|
||||
|
||||
// Обновляем coordinator с актуальной высотой клавиатуры
|
||||
android.util.Log.d("KeyboardHeight", "🔄 Updating coordinator...")
|
||||
coordinator.updateKeyboardHeight(imeHeight)
|
||||
|
||||
// Синхронизируем высоту emoji с клавиатурой
|
||||
if (imeHeight > 100.dp) {
|
||||
android.util.Log.d("KeyboardHeight", "🔄 Syncing heights...")
|
||||
coordinator.syncHeights()
|
||||
}
|
||||
android.util.Log.d("KeyboardHeight", "✅ Coordinator updated")
|
||||
}
|
||||
|
||||
// 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences)
|
||||
@@ -2067,71 +2097,45 @@ private fun MessageInputBar(
|
||||
focusManager.clearFocus(force = true)
|
||||
}
|
||||
|
||||
// 🔥 Функция переключения emoji picker - МАКСИМАЛЬНО АГРЕССИВНОЕ ОТКРЫТИЕ КЛАВИАТУРЫ
|
||||
// 🔥 Функция переключения emoji picker с Telegram-style transitions
|
||||
fun toggleEmojiPicker() {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
android.util.Log.d("EmojiPicker", "=".repeat(60))
|
||||
android.util.Log.d("EmojiPicker", "🔥 toggleEmojiPicker START")
|
||||
android.util.Log.d("EmojiPicker", " State: showEmojiPicker=$showEmojiPicker, isKeyboardVisible=$isKeyboardVisible")
|
||||
android.util.Log.d("EmojiPicker", " IME height: $imeHeight, editTextView=${if (editTextView != null) "SET" else "NULL"}")
|
||||
android.util.Log.d("EmojiPicker", " showEmojiPicker(local)=$showEmojiPicker")
|
||||
coordinator.logState()
|
||||
|
||||
if (showEmojiPicker) {
|
||||
// ========== ЗАКРЫВАЕМ EMOJI → ОТКРЫВАЕМ КЛАВИАТУРУ ==========
|
||||
android.util.Log.d("EmojiPicker", "📱 Action: CLOSING emoji → OPENING keyboard")
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
// Шаг 1: Закрываем emoji панель
|
||||
onToggleEmojiPicker(false)
|
||||
android.util.Log.d("EmojiPicker", " [1] Emoji panel closed")
|
||||
|
||||
// Шаг 2: Немедленно фокусируем и открываем клавиатуру
|
||||
editTextView?.let { editText ->
|
||||
editText.requestFocus()
|
||||
|
||||
// Метод 1: Немедленный вызов
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
|
||||
android.util.Log.d("EmojiPicker", " [2] Method 1: showSoftInput(FORCED) called")
|
||||
|
||||
// Метод 2: Через post (следующий frame)
|
||||
view.post {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
android.util.Log.d("EmojiPicker", " [3] Method 2: showSoftInput(IMPLICIT) called (${elapsed}ms)")
|
||||
}
|
||||
|
||||
// Метод 3: Через postDelayed (100ms)
|
||||
view.postDelayed({
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
|
||||
android.util.Log.d("EmojiPicker", " [4] Method 3: toggleSoftInput called (${elapsed}ms)")
|
||||
}, 100)
|
||||
|
||||
// Метод 4: Финальная попытка через 200ms
|
||||
view.postDelayed({
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
if (!isKeyboardVisible) {
|
||||
android.util.Log.w("EmojiPicker", " [5] ⚠️ Keyboard still not visible after ${elapsed}ms, forcing again")
|
||||
// 🔥 ИСПОЛЬЗУЕМ coordinator.isEmojiVisible вместо showEmojiPicker для более точного состояния
|
||||
if (coordinator.isEmojiVisible) {
|
||||
// ========== EMOJI → KEYBOARD ==========
|
||||
android.util.Log.d("EmojiPicker", "🔄 Switching: Emoji → Keyboard")
|
||||
coordinator.requestShowKeyboard(
|
||||
showKeyboard = {
|
||||
editTextView?.let { editText ->
|
||||
editText.requestFocus()
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
|
||||
} else {
|
||||
android.util.Log.d("EmojiPicker", " [5] ✅ Keyboard opened successfully in ${elapsed}ms!")
|
||||
android.util.Log.d("EmojiPicker", "📱 Keyboard show requested")
|
||||
}
|
||||
}, 200)
|
||||
} ?: android.util.Log.e("EmojiPicker", " ❌ ERROR: editTextView is null!")
|
||||
},
|
||||
hideEmoji = {
|
||||
onToggleEmojiPicker(false)
|
||||
android.util.Log.d("EmojiPicker", "😊 Emoji panel hidden")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// ========== ОТКРЫВАЕМ EMOJI → ЗАКРЫВАЕМ КЛАВИАТУРУ ==========
|
||||
android.util.Log.d("EmojiPicker", "😊 Action: OPENING emoji → CLOSING keyboard")
|
||||
|
||||
// Шаг 1: Скрываем клавиатуру
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
android.util.Log.d("EmojiPicker", " [1] Keyboard hide requested")
|
||||
|
||||
// Шаг 2: Небольшая задержка для плавности
|
||||
view.postDelayed({
|
||||
// Шаг 3: Открываем emoji панель
|
||||
onToggleEmojiPicker(true)
|
||||
android.util.Log.d("EmojiPicker", " [2] ✅ Emoji panel opened (50ms delay)")
|
||||
}, 50)
|
||||
// ========== KEYBOARD → EMOJI ==========
|
||||
android.util.Log.d("EmojiPicker", "🔄 Switching: Keyboard → Emoji")
|
||||
coordinator.requestShowEmoji(
|
||||
hideKeyboard = {
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
android.util.Log.d("EmojiPicker", "⌨️ Keyboard hide requested")
|
||||
},
|
||||
showEmoji = {
|
||||
onToggleEmojiPicker(true)
|
||||
android.util.Log.d("EmojiPicker", "😊 Emoji panel shown")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
android.util.Log.d("EmojiPicker", "🔥 toggleEmojiPicker END")
|
||||
@@ -2309,26 +2313,6 @@ private fun MessageInputBar(
|
||||
.background(
|
||||
color = backgroundColor // Тот же цвет что и фон чата
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
// При клике на инпут - закрываем эмодзи панель и открываем клавиатуру
|
||||
if (showEmojiPicker) {
|
||||
onToggleEmojiPicker(false)
|
||||
// Открываем клавиатуру после небольшой задержки
|
||||
view.postDelayed({
|
||||
editTextView?.let { editText ->
|
||||
editText.requestFocus()
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
|
||||
}
|
||||
}, 100)
|
||||
} else {
|
||||
// Просто фокусируем для открытия клавиатуры
|
||||
editTextView?.requestFocus()
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.TopStart // 🔥 TopStart чтобы текст не обрезался
|
||||
) {
|
||||
@@ -2344,7 +2328,29 @@ private fun MessageInputBar(
|
||||
onViewCreated = { view ->
|
||||
// 🔥 Сохраняем ссылку на EditText для программного открытия клавиатуры
|
||||
editTextView = view
|
||||
android.util.Log.d("EmojiPicker", "✅ editTextView set: $view")
|
||||
},
|
||||
onFocusChanged = { hasFocus ->
|
||||
android.util.Log.d("EmojiPicker", "═══════════════════════════════════════════════════════")
|
||||
android.util.Log.d("EmojiPicker", "🎯 TextField focus changed: hasFocus=$hasFocus")
|
||||
android.util.Log.d("EmojiPicker", " 📊 Current state:")
|
||||
android.util.Log.d("EmojiPicker", " - showEmojiPicker=$showEmojiPicker")
|
||||
android.util.Log.d("EmojiPicker", " - coordinator.isEmojiVisible=${coordinator.isEmojiVisible}")
|
||||
android.util.Log.d("EmojiPicker", " - coordinator.isKeyboardVisible=${coordinator.isKeyboardVisible}")
|
||||
android.util.Log.d("EmojiPicker", " - coordinator.currentState=${coordinator.currentState}")
|
||||
|
||||
// Если TextField получил фокус И emoji открыт → закрываем emoji
|
||||
if (hasFocus && showEmojiPicker) {
|
||||
android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji")
|
||||
android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...")
|
||||
onToggleEmojiPicker(false)
|
||||
android.util.Log.d("EmojiPicker", " 📞 Setting coordinator.isEmojiVisible = false...")
|
||||
coordinator.isEmojiVisible = false
|
||||
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
|
||||
} else if (hasFocus && !showEmojiPicker) {
|
||||
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")
|
||||
} else if (!hasFocus) {
|
||||
android.util.Log.d("EmojiPicker", "👋 TextField lost focus")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -2391,22 +2397,25 @@ private fun MessageInputBar(
|
||||
} // End of else (not blocked)
|
||||
|
||||
|
||||
// 🔥 ОПТИМИЗИРОВАННЫЙ EMOJI PICKER - с предзагрузкой и smooth animations
|
||||
// 🔥 EMOJI PICKER с плавными Telegram-style анимациями
|
||||
if (!isBlocked) {
|
||||
// Новый оптимизированный пикер автоматически управляет анимациями
|
||||
OptimizedEmojiPicker(
|
||||
isVisible = showEmojiPicker && !isKeyboardVisible,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = { emoji ->
|
||||
onValueChange(value + emoji)
|
||||
},
|
||||
onClose = {
|
||||
// 🔥 Используем toggleEmojiPicker() - он закроет панель и откроет клавиатуру
|
||||
toggleEmojiPicker()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
AnimatedKeyboardTransition(
|
||||
coordinator = coordinator,
|
||||
showEmojiPicker = showEmojiPicker
|
||||
) {
|
||||
OptimizedEmojiPicker(
|
||||
isVisible = true, // Видимость контролирует AnimatedKeyboardTransition
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = { emoji ->
|
||||
onValueChange(value + emoji)
|
||||
},
|
||||
onClose = {
|
||||
// Используем coordinator для плавного перехода
|
||||
toggleEmojiPicker()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} // End of if (!isBlocked) for emoji picker
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,8 @@ fun AppleEmojiTextField(
|
||||
hint: String = "Message",
|
||||
hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray,
|
||||
onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null,
|
||||
requestFocus: Boolean = false
|
||||
requestFocus: Boolean = false,
|
||||
onFocusChanged: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
// Храним ссылку на view для управления фокусом
|
||||
var editTextView by remember { mutableStateOf<AppleEmojiEditTextView?>(null) }
|
||||
@@ -236,6 +237,14 @@ fun AppleEmojiTextField(
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
// Сохраняем ссылку на view
|
||||
editTextView = this
|
||||
// Подключаем callback для изменения фокуса
|
||||
setOnFocusChangeListener { _, hasFocus ->
|
||||
android.util.Log.d("AppleEmojiTextField", "═══════════════════════════════════════════════════════")
|
||||
android.util.Log.d("AppleEmojiTextField", "🎯 Native EditText focus changed: hasFocus=$hasFocus")
|
||||
android.util.Log.d("AppleEmojiTextField", " 📍 Calling onFocusChanged callback...")
|
||||
onFocusChanged?.invoke(hasFocus)
|
||||
android.util.Log.d("AppleEmojiTextField", " ✅ onFocusChanged callback completed")
|
||||
}
|
||||
// Уведомляем о создании view
|
||||
onViewCreated?.invoke(this)
|
||||
}
|
||||
|
||||
@@ -45,15 +45,15 @@ import kotlinx.coroutines.launch
|
||||
* 2. LazyGrid с оптимизированными настройками (beyondBoundsLayout)
|
||||
* 3. Hardware layer для анимаций
|
||||
* 4. Минимум recomposition (derivedStateOf, remember keys)
|
||||
* 5. Smooth slide + fade transitions (Telegram-style)
|
||||
* 6. Coil оптимизация (hardware acceleration, size limits)
|
||||
* 7. SharedPreferences для сохранения высоты клавиатуры (как в Telegram)
|
||||
* 8. keyboardDuration для синхронизации с системной клавиатурой
|
||||
* 5. Coil оптимизация (hardware acceleration, size limits)
|
||||
* 6. SharedPreferences для сохранения высоты клавиатуры (как в Telegram)
|
||||
* 7. keyboardDuration для синхронизации с системной клавиатурой
|
||||
* 8. Анимация управляется внешним AnimatedKeyboardTransition
|
||||
*
|
||||
* @param isVisible Видимость панели
|
||||
* @param isVisible Видимость панели (для внутренней логики)
|
||||
* @param isDarkTheme Темная/светлая тема
|
||||
* @param onEmojiSelected Callback при выборе эмодзи
|
||||
* @param onClose Callback при закрытии (не используется, панель просто скрывается)
|
||||
* @param onClose Callback при закрытии
|
||||
* @param modifier Модификатор
|
||||
*/
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@@ -68,59 +68,19 @@ fun OptimizedEmojiPicker(
|
||||
// 🔥 Используем сохранённую высоту клавиатуры (как в Telegram)
|
||||
val savedKeyboardHeight = rememberSavedKeyboardHeight()
|
||||
|
||||
// 🔥 Telegram's keyboardDuration для синхронизации анимации
|
||||
val animationDuration = KeyboardHeightProvider.getKeyboardAnimationDuration().toInt()
|
||||
|
||||
// 🔥 Логирование изменений видимости
|
||||
LaunchedEffect(isVisible) {
|
||||
android.util.Log.d("EmojiPicker", "🎭 OptimizedEmojiPicker visibility changed: $isVisible (height=${savedKeyboardHeight}, animDuration=${animationDuration}ms)")
|
||||
android.util.Log.d("EmojiPicker", "🎭 OptimizedEmojiPicker visibility: $isVisible (height=${savedKeyboardHeight})")
|
||||
}
|
||||
|
||||
// 🎭 Telegram-style анимация: используем сохранённую длительность
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDuration, // 🔥 Telegram's 250ms
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
) + fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDuration / 2,
|
||||
easing = LinearEasing
|
||||
)
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = tween(
|
||||
durationMillis = (animationDuration * 0.8).toInt(), // 🔥 Быстрое закрытие (200ms)
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
) + fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = (animationDuration * 0.6).toInt(),
|
||||
easing = LinearEasing
|
||||
)
|
||||
),
|
||||
// 🔥 Рендерим контент напрямую без AnimatedVisibility
|
||||
// Анимация теперь управляется AnimatedKeyboardTransition
|
||||
EmojiPickerContent(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = onEmojiSelected,
|
||||
keyboardHeight = savedKeyboardHeight,
|
||||
modifier = modifier
|
||||
) {
|
||||
// 🎨 Hardware layer для анимаций (GPU ускорение - как в Telegram)
|
||||
Box(
|
||||
modifier = Modifier.graphicsLayer {
|
||||
// Используем hardware layer только во время анимации
|
||||
if (transition.isRunning) {
|
||||
this.alpha = 1f
|
||||
}
|
||||
}
|
||||
) {
|
||||
EmojiPickerContent(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = onEmojiSelected,
|
||||
keyboardHeight = savedKeyboardHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +90,8 @@ fun OptimizedEmojiPicker(
|
||||
private fun EmojiPickerContent(
|
||||
isDarkTheme: Boolean,
|
||||
onEmojiSelected: (String) -> Unit,
|
||||
keyboardHeight: Dp
|
||||
keyboardHeight: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
||||
@@ -185,7 +146,7 @@ private fun EmojiPickerContent(
|
||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height = keyboardHeight) // 🔥 Используем сохранённую высоту (как в Telegram)
|
||||
.background(panelBackground)
|
||||
|
||||
Reference in New Issue
Block a user