feat: Update ChatDetailScreen to use inputBackgroundColor for dropdown menus and menu items
This commit is contained in:
@@ -1,31 +1,33 @@
|
|||||||
package app.rosette.android.ui.keyboard
|
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.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
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.foundation.layout.offset
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
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 androidx.compose.ui.unit.dp
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Анимированный контейнер для emoji панели с Telegram-style переходами.
|
* 🚀 МЕТОД: Animated Height + Content Offset (Telegram-style)
|
||||||
*
|
*
|
||||||
* Паттерны Telegram:
|
* Ключевые принципы:
|
||||||
* - Keyboard → Emoji: панель выезжает снизу за 250ms (FastOutSlowIn)
|
* 1. ВЫСОТА контейнера анимируется от 0dp до emojiHeight
|
||||||
* - Emoji → Keyboard: панель уезжает вниз за 200ms (FastOutLinearIn)
|
* 2. Контент внутри clipToBounds - обрезается по границам
|
||||||
* - TranslationY + Alpha для плавности
|
* 3. НЕТ предварительного резервирования места когда скрыт
|
||||||
* - Немедленное резервирование места
|
* 4. graphicsLayer для GPU-ускорения
|
||||||
|
*
|
||||||
|
* Паттерн:
|
||||||
|
* - Показать: height анимируется от 0dp до emojiHeight (контент появляется снизу)
|
||||||
|
* - Скрыть: height анимируется от emojiHeight до 0dp (контент уходит вниз)
|
||||||
|
* - Длительность: 250ms с FastOutSlowInEasing
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedKeyboardTransition(
|
fun AnimatedKeyboardTransition(
|
||||||
@@ -35,97 +37,65 @@ fun AnimatedKeyboardTransition(
|
|||||||
) {
|
) {
|
||||||
val tag = "AnimatedTransition"
|
val tag = "AnimatedTransition"
|
||||||
|
|
||||||
// Animatable для плавной анимации offset
|
// 🎯 Целевая высота: emojiHeight = видно, 0dp = скрыто
|
||||||
val scope = rememberCoroutineScope()
|
val targetHeight = if (showEmojiPicker) {
|
||||||
val offsetY = remember { Animatable(0f) }
|
coordinator.emojiHeight
|
||||||
val alpha = remember { Animatable(1f) }
|
} else {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
|
|
||||||
// Отслеживаем изменения showEmojiPicker
|
// 🎬 Декларативная анимация высоты
|
||||||
LaunchedEffect(showEmojiPicker) {
|
val animatedHeight by animateDpAsState(
|
||||||
Log.d(tag, "════════════════════════════════════════════════════════")
|
targetValue = targetHeight,
|
||||||
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(
|
animationSpec = tween(
|
||||||
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
|
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
|
||||||
easing = FastOutSlowInEasing
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "emojiPanelHeight"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
Log.d(tag, "✅ Emoji slide animation completed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade in (немного быстрее)
|
Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, targetH=$targetHeight, currentH=$animatedHeight, emojiH=${coordinator.emojiHeight}")
|
||||||
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 ============
|
if (animatedHeight > 0.dp) {
|
||||||
Log.d(tag, "📥 Hiding emoji panel (offsetY=${offsetY.value}dp < threshold=${coordinator.emojiHeight.value * 0.9f}dp)")
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
val height = coordinator.emojiHeight.value
|
.fillMaxWidth()
|
||||||
Log.d(tag, "🎯 Animating emoji OUT from ${offsetY.value}dp to ${height}dp")
|
.height(animatedHeight)
|
||||||
Log.d(tag, "⏱️ Duration: 200ms, Easing: FastOutLinearIn")
|
.clipToBounds() // 🔥 Обрезаем контент по границам контейнера
|
||||||
scope.launch {
|
.graphicsLayer {
|
||||||
offsetY.animateTo(
|
// GPU-ускорение для плавности
|
||||||
targetValue = height,
|
clip = true
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 200,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Log.d(tag, "✅ Emoji hide animation completed")
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
// Fade out (быстрее)
|
// Внутренний контейнер с полной высотой emoji
|
||||||
Log.d(tag, "🌑 Starting fade out animation (150ms)")
|
// Он обрезается clipToBounds родителя
|
||||||
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(coordinator.emojiHeight)
|
.height(coordinator.emojiHeight)
|
||||||
.offset(y = offsetY.value.dp)
|
// 🔥 Позиционируем снизу: когда animatedHeight < emojiHeight,
|
||||||
.alpha(alpha.value)
|
// верхняя часть обрезается, создавая эффект "выезда снизу"
|
||||||
|
.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()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Упрощенная версия без fade анимации.
|
* Алиас для обратной совместимости
|
||||||
* Только slide (как в оригинальном Telegram).
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleAnimatedKeyboardTransition(
|
fun SimpleAnimatedKeyboardTransition(
|
||||||
@@ -133,39 +103,9 @@ fun SimpleAnimatedKeyboardTransition(
|
|||||||
showEmojiPicker: Boolean,
|
showEmojiPicker: Boolean,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val offsetY = remember { Animatable(0f) }
|
AnimatedKeyboardTransition(
|
||||||
|
coordinator = coordinator,
|
||||||
LaunchedEffect(showEmojiPicker) {
|
showEmojiPicker = showEmojiPicker,
|
||||||
if (showEmojiPicker) {
|
content = content
|
||||||
// Показать: снизу вверх
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,9 @@ class KeyboardTransitionCoordinator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Переход от системной клавиатуры к emoji панели.
|
* Переход от системной клавиатуры к emoji панели.
|
||||||
* Telegram паттерн: сначала скрыть клавиатуру, затем показать emoji.
|
*
|
||||||
|
* 🔥 КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Показываем emoji СРАЗУ!
|
||||||
|
* Не ждем закрытия клавиатуры - emoji начинает выезжать синхронно.
|
||||||
*/
|
*/
|
||||||
fun requestShowEmoji(
|
fun requestShowEmoji(
|
||||||
hideKeyboard: () -> Unit,
|
hideKeyboard: () -> Unit,
|
||||||
@@ -84,40 +86,25 @@ class KeyboardTransitionCoordinator {
|
|||||||
Log.d(TAG, " 📊 Current state:")
|
Log.d(TAG, " 📊 Current state:")
|
||||||
Log.d(TAG, " - currentState=$currentState")
|
Log.d(TAG, " - currentState=$currentState")
|
||||||
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
|
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
|
||||||
Log.d(TAG, " - isTransitioning=$isTransitioning")
|
Log.d(TAG, " - maxKeyboardHeight=$maxKeyboardHeight")
|
||||||
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
|
currentState = TransitionState.KEYBOARD_TO_EMOJI
|
||||||
isTransitioning = true
|
isTransitioning = true
|
||||||
|
|
||||||
// 🔥 Сохраняем коллбэк для вызова после закрытия клавиатуры
|
// 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры)
|
||||||
Log.d(TAG, " 💾 Saving pending emoji callback...")
|
if (maxKeyboardHeight > 0.dp) {
|
||||||
pendingShowEmojiCallback = {
|
emojiHeight = maxKeyboardHeight
|
||||||
try {
|
Log.d(TAG, " 📌 Locked emojiHeight to maxKeyboardHeight: $emojiHeight")
|
||||||
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: Скрыть системную клавиатуру
|
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
|
||||||
Log.d(TAG, " 📞 Calling hideKeyboard()...")
|
Log.d(TAG, " 🚀 IMMEDIATELY showing emoji (no waiting for keyboard close)")
|
||||||
|
showEmoji()
|
||||||
|
isEmojiVisible = true
|
||||||
|
Log.d(TAG, " ✅ showEmoji() called, isEmojiVisible=true")
|
||||||
|
|
||||||
|
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
|
||||||
|
Log.d(TAG, " ⌨️ Hiding keyboard...")
|
||||||
try {
|
try {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
Log.d(TAG, " ✅ hideKeyboard() completed")
|
Log.d(TAG, " ✅ hideKeyboard() completed")
|
||||||
@@ -125,12 +112,14 @@ class KeyboardTransitionCoordinator {
|
|||||||
Log.e(TAG, "❌ Error hiding keyboard", e)
|
Log.e(TAG, "❌ Error hiding keyboard", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если клавиатура уже закрыта (height = 0), показываем emoji сразу
|
isKeyboardVisible = false
|
||||||
if (keyboardHeight <= 0.dp) {
|
currentState = TransitionState.IDLE
|
||||||
Log.d(TAG, "⚡ Keyboard already closed, showing emoji immediately")
|
isTransitioning = false
|
||||||
pendingShowEmojiCallback?.invoke()
|
|
||||||
}
|
// Очищаем pending callback - больше не нужен
|
||||||
// Иначе ждем когда updateKeyboardHeight получит 0
|
pendingShowEmojiCallback = null
|
||||||
|
|
||||||
|
Log.d(TAG, "✅ requestShowEmoji() completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Главный метод: Emoji → Keyboard ============
|
// ============ Главный метод: Emoji → Keyboard ============
|
||||||
@@ -293,27 +282,13 @@ class KeyboardTransitionCoordinator {
|
|||||||
Log.d(TAG, " After: emojiHeight restored to $maxKeyboardHeight")
|
Log.d(TAG, " After: emojiHeight restored to $maxKeyboardHeight")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обнуляем keyboardHeight только после восстановления emoji
|
// Обнуляем keyboardHeight
|
||||||
keyboardHeight = 0.dp
|
keyboardHeight = 0.dp
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "⏭️ No update needed (height too small or unchanged)")
|
Log.d(TAG, "⏭️ No update needed (height too small or unchanged)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Если клавиатура закрылась И есть pending коллбэк → показываем emoji
|
// 🔥 УБРАН pending callback - теперь emoji показывается сразу в requestShowEmoji()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -328,15 +303,29 @@ class KeyboardTransitionCoordinator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Синхронизировать высоты (emoji = keyboard).
|
* Синхронизировать высоты (emoji = keyboard).
|
||||||
|
*
|
||||||
|
* 🔥 ВАЖНО: Синхронизируем ТОЛЬКО когда клавиатура ОТКРЫВАЕТСЯ!
|
||||||
|
* При закрытии клавиатуры emojiHeight должна оставаться фиксированной!
|
||||||
*/
|
*/
|
||||||
fun syncHeights() {
|
fun syncHeights() {
|
||||||
Log.d(TAG, "🔄 syncHeights called: keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
|
// 🔥 Синхронизируем ТОЛЬКО если клавиатура ОТКРЫТА и высота больше текущей emoji
|
||||||
if (keyboardHeight > 100.dp) {
|
if (keyboardHeight > 100.dp && keyboardHeight > emojiHeight) {
|
||||||
val oldEmojiHeight = emojiHeight
|
Log.d(TAG, "🔄 syncHeights: updating emoji $emojiHeight → $keyboardHeight")
|
||||||
emojiHeight = keyboardHeight
|
emojiHeight = keyboardHeight
|
||||||
Log.d(TAG, "✅ Heights synced: $oldEmojiHeight → $keyboardHeight")
|
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "⏭️ Keyboard height too small ($keyboardHeight), skipping sync")
|
Log.d(TAG, "⏭️ syncHeights: skipped (keyboard=$keyboardHeight, emoji=$emojiHeight)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация высоты emoji панели (для pre-rendered подхода).
|
||||||
|
* Должна быть вызвана при старте для избежания 0dp высоты.
|
||||||
|
*/
|
||||||
|
fun initializeEmojiHeight(height: Dp) {
|
||||||
|
if (emojiHeight == 0.dp && height > 0.dp) {
|
||||||
|
Log.d(TAG, "🚀 Initializing emoji height: $height")
|
||||||
|
emojiHeight = height
|
||||||
|
maxKeyboardHeight = height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -71,6 +72,7 @@ import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
|||||||
import com.rosetta.messenger.ui.components.VerifiedBadge
|
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||||
|
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
|
||||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -219,6 +221,8 @@ fun ChatDetailScreen(
|
|||||||
onUserProfileClick: () -> Unit = {},
|
onUserProfileClick: () -> Unit = {},
|
||||||
viewModel: ChatViewModel = viewModel()
|
viewModel: ChatViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
|
// 🔥 ОПТИМИЗАЦИЯ: Убрано логирование из композиции чтобы не засорять logcat
|
||||||
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
||||||
@@ -247,26 +251,36 @@ fun ChatDetailScreen(
|
|||||||
|
|
||||||
// 🔥 Emoji picker state (поднят из MessageInputBar для KeyboardAvoidingView)
|
// 🔥 Emoji picker state (поднят из MessageInputBar для KeyboardAvoidingView)
|
||||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||||
// Высота эмодзи панели - берём высоту клавиатуры если она открыта, иначе 280dp
|
|
||||||
|
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
|
||||||
|
val coordinator = rememberKeyboardTransitionCoordinator()
|
||||||
|
|
||||||
|
// 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую в композиции!
|
||||||
|
// Используем snapshotFlow чтобы избежать рекомпозиции на каждый пиксель анимации
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
|
|
||||||
val isKeyboardVisible = imeHeight > 50.dp
|
|
||||||
|
|
||||||
// 🔥 Запоминаем высоту клавиатуры когда она открыта
|
// 🔥 Синхронизируем coordinator с IME высотой через snapshotFlow (БЕЗ рекомпозиции!)
|
||||||
var savedKeyboardHeight by remember { mutableStateOf(280.dp) }
|
LaunchedEffect(Unit) {
|
||||||
LaunchedEffect(imeHeight) {
|
snapshotFlow {
|
||||||
if (imeHeight > 50.dp) {
|
with(density) { imeInsets.getBottom(density).toDp() }
|
||||||
savedKeyboardHeight = imeHeight
|
}.collect { currentImeHeight ->
|
||||||
|
coordinator.updateKeyboardHeight(currentImeHeight)
|
||||||
|
if (currentImeHeight > 100.dp) {
|
||||||
|
coordinator.syncHeights()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 🔥 Высота панели эмодзи = сохранённая высота клавиатуры (минимум 280.dp)
|
|
||||||
val emojiPanelHeight = maxOf(savedKeyboardHeight, 280.dp)
|
|
||||||
|
|
||||||
// 🔥 Флаг видимости панели эмодзи (тот же что в MessageInputBar) - единый источник правды
|
// 🔥 Инициализируем высоту emoji панели из сохранённой высоты клавиатуры
|
||||||
val isEmojiPanelVisible = showEmojiPicker && !isKeyboardVisible
|
LaunchedEffect(Unit) {
|
||||||
|
val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
||||||
// <20> Простой отступ без анимации - AnimatedVisibility сама анимирует
|
if (savedHeightPx > 0) {
|
||||||
val emojiPanelPadding = if (isEmojiPanelVisible) emojiPanelHeight else 0.dp
|
val savedHeightDp = with(density) { savedHeightPx.toDp() }
|
||||||
|
coordinator.initializeEmojiHeight(savedHeightDp)
|
||||||
|
} else {
|
||||||
|
coordinator.initializeEmojiHeight(280.dp) // fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 Reply/Forward state
|
// 🔥 Reply/Forward state
|
||||||
val replyMessages by viewModel.replyMessages.collectAsState()
|
val replyMessages by viewModel.replyMessages.collectAsState()
|
||||||
@@ -736,7 +750,7 @@ fun ChatDetailScreen(
|
|||||||
// Выпадающее меню - чистый дизайн без артефактов
|
// Выпадающее меню - чистый дизайн без артефактов
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = MaterialTheme.colorScheme.copy(
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
surface = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
|
surface = inputBackgroundColor
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -745,7 +759,7 @@ fun ChatDetailScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(220.dp)
|
.width(220.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White,
|
color = inputBackgroundColor,
|
||||||
shape = RoundedCornerShape(16.dp)
|
shape = RoundedCornerShape(16.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -775,7 +789,8 @@ fun ChatDetailScreen(
|
|||||||
showMenu = false
|
showMenu = false
|
||||||
showDeleteConfirm = true
|
showDeleteConfirm = true
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
|
.background(inputBackgroundColor),
|
||||||
colors = MenuDefaults.itemColors(
|
colors = MenuDefaults.itemColors(
|
||||||
textColor = Color(0xFFE53935)
|
textColor = Color(0xFFE53935)
|
||||||
)
|
)
|
||||||
@@ -822,7 +837,8 @@ fun ChatDetailScreen(
|
|||||||
showBlockConfirm = true
|
showBlockConfirm = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
|
.background(inputBackgroundColor),
|
||||||
colors = MenuDefaults.itemColors(
|
colors = MenuDefaults.itemColors(
|
||||||
textColor = PrimaryBlue
|
textColor = PrimaryBlue
|
||||||
)
|
)
|
||||||
@@ -863,7 +879,8 @@ fun ChatDetailScreen(
|
|||||||
showMenu = false
|
showMenu = false
|
||||||
showLogs = true
|
showLogs = true
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
|
.background(inputBackgroundColor),
|
||||||
colors = MenuDefaults.itemColors(
|
colors = MenuDefaults.itemColors(
|
||||||
textColor = textColor
|
textColor = textColor
|
||||||
)
|
)
|
||||||
@@ -937,7 +954,9 @@ fun ChatDetailScreen(
|
|||||||
showEmojiPicker = showEmojiPicker,
|
showEmojiPicker = showEmojiPicker,
|
||||||
onToggleEmojiPicker = { showEmojiPicker = it },
|
onToggleEmojiPicker = { showEmojiPicker = it },
|
||||||
// Focus requester для автофокуса при reply
|
// Focus requester для автофокуса при reply
|
||||||
focusRequester = inputFocusRequester
|
focusRequester = inputFocusRequester,
|
||||||
|
// Coordinator для плавных переходов
|
||||||
|
coordinator = coordinator
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1978,8 +1997,12 @@ private fun MessageInputBar(
|
|||||||
showEmojiPicker: Boolean = false,
|
showEmojiPicker: Boolean = false,
|
||||||
onToggleEmojiPicker: (Boolean) -> Unit = {},
|
onToggleEmojiPicker: (Boolean) -> Unit = {},
|
||||||
// Focus requester для автофокуса при reply
|
// Focus requester для автофокуса при reply
|
||||||
focusRequester: FocusRequester? = null
|
focusRequester: FocusRequester? = null,
|
||||||
|
// Coordinator для плавных переходов клавиатуры
|
||||||
|
coordinator: KeyboardTransitionCoordinator
|
||||||
) {
|
) {
|
||||||
|
// 🔥 ОПТИМИЗАЦИЯ: Убрано логирование чтобы не засорять logcat
|
||||||
|
|
||||||
val hasReply = replyMessages.isNotEmpty()
|
val hasReply = replyMessages.isNotEmpty()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -1991,9 +2014,6 @@ private fun MessageInputBar(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
|
|
||||||
val coordinator = rememberKeyboardTransitionCoordinator()
|
|
||||||
|
|
||||||
// 🔥 Ссылка на EditText для программного фокуса
|
// 🔥 Ссылка на EditText для программного фокуса
|
||||||
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
|
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
|
||||||
|
|
||||||
@@ -2021,56 +2041,47 @@ private fun MessageInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Отслеживаем высоту клавиатуры (Telegram-style)
|
// 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую - это вызывает рекомпозицию!
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
|
|
||||||
val isKeyboardVisible = imeHeight > 50.dp // 🔥 Согласованный порог с ChatDetailScreen
|
|
||||||
|
|
||||||
// 🔥 Флаг "клавиатура в процессе анимации"
|
// 🔥 Флаг "клавиатура видна" - обновляется через snapshotFlow, НЕ вызывает рекомпозицию
|
||||||
var isKeyboardAnimating by remember { mutableStateOf(false) }
|
var isKeyboardVisible by remember { mutableStateOf(false) }
|
||||||
|
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
|
||||||
|
|
||||||
// 🔥 Логирование изменений высоты клавиатуры + обновление coordinator
|
// 🔥 Обновляем coordinator через snapshotFlow (БЕЗ рекомпозиции!)
|
||||||
LaunchedEffect(imeHeight) {
|
LaunchedEffect(Unit) {
|
||||||
android.util.Log.d("KeyboardHeight", "═══════════════════════════════════════════════════════")
|
snapshotFlow {
|
||||||
android.util.Log.d("KeyboardHeight", "📊 IME height changed: $imeHeight")
|
with(density) { imeInsets.getBottom(density).toDp() }
|
||||||
android.util.Log.d("KeyboardHeight", " isKeyboardVisible=$isKeyboardVisible")
|
}.collect { currentImeHeight ->
|
||||||
android.util.Log.d("KeyboardHeight", " showEmojiPicker=$showEmojiPicker")
|
// Обновляем флаг видимости (это НЕ вызывает рекомпозицию напрямую)
|
||||||
android.util.Log.d("KeyboardHeight", " isKeyboardAnimating=$isKeyboardAnimating")
|
isKeyboardVisible = currentImeHeight > 50.dp
|
||||||
|
|
||||||
// Обновляем coordinator с актуальной высотой клавиатуры
|
// Обновляем coordinator
|
||||||
android.util.Log.d("KeyboardHeight", "🔄 Updating coordinator...")
|
coordinator.updateKeyboardHeight(currentImeHeight)
|
||||||
coordinator.updateKeyboardHeight(imeHeight)
|
if (currentImeHeight > 100.dp) {
|
||||||
|
|
||||||
// Синхронизируем высоту emoji с клавиатурой
|
|
||||||
if (imeHeight > 100.dp) {
|
|
||||||
android.util.Log.d("KeyboardHeight", "🔄 Syncing heights...")
|
|
||||||
coordinator.syncHeights()
|
coordinator.syncHeights()
|
||||||
|
lastStableKeyboardHeight = currentImeHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
android.util.Log.d("KeyboardHeight", "✅ Coordinator updated")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences)
|
// 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Загружаем сохранённую высоту при старте
|
// Загружаем сохранённую высоту при старте
|
||||||
val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
||||||
android.util.Log.d("KeyboardHeight", "📱 MessageInputBar initialized, loaded height: ${savedHeightPx}px")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Сохраняем высоту ТОЛЬКО когда клавиатура стабильна
|
// 🔥 Сохраняем высоту ТОЛЬКО когда клавиатура стабильна
|
||||||
// Используем отдельный LaunchedEffect который НЕ реагирует на каждое изменение
|
|
||||||
LaunchedEffect(isKeyboardVisible, showEmojiPicker) {
|
LaunchedEffect(isKeyboardVisible, showEmojiPicker) {
|
||||||
// Если клавиатура стала видимой и emoji закрыт
|
// Если клавиатура стала видимой и emoji закрыт
|
||||||
if (isKeyboardVisible && !showEmojiPicker && !isKeyboardAnimating) {
|
if (isKeyboardVisible && !showEmojiPicker) {
|
||||||
// Ждем стабилизации
|
// Ждем стабилизации
|
||||||
isKeyboardAnimating = true
|
kotlinx.coroutines.delay(350) // Анимация клавиатуры ~300ms
|
||||||
kotlinx.coroutines.delay(300) // Анимация клавиатуры ~250ms
|
|
||||||
isKeyboardAnimating = false
|
|
||||||
|
|
||||||
// Сохраняем только если всё еще видна и emoji закрыт
|
// Сохраняем только если всё еще видна и emoji закрыт
|
||||||
if (isKeyboardVisible && !showEmojiPicker && imeHeight > 300.dp) {
|
if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) {
|
||||||
val heightPx = with(density) { imeHeight.toPx().toInt() }
|
val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() }
|
||||||
com.rosetta.messenger.ui.components.KeyboardHeightProvider.saveKeyboardHeight(context, heightPx)
|
com.rosetta.messenger.ui.components.KeyboardHeightProvider.saveKeyboardHeight(context, heightPx)
|
||||||
android.util.Log.d("KeyboardHeight", "✅ Stable keyboard height saved: ${imeHeight} (${heightPx}px)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2343,8 +2354,6 @@ private fun MessageInputBar(
|
|||||||
android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji")
|
android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji")
|
||||||
android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...")
|
android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...")
|
||||||
onToggleEmojiPicker(false)
|
onToggleEmojiPicker(false)
|
||||||
android.util.Log.d("EmojiPicker", " 📞 Setting coordinator.isEmojiVisible = false...")
|
|
||||||
coordinator.isEmojiVisible = false
|
|
||||||
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
|
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
|
||||||
} else if (hasFocus && !showEmojiPicker) {
|
} else if (hasFocus && !showEmojiPicker) {
|
||||||
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")
|
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")
|
||||||
|
|||||||
Reference in New Issue
Block a user