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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user