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:
k1ngsterr1
2026-01-15 12:08:10 +05:00
parent a075f98dcb
commit 9f4e85d64a
6 changed files with 1300 additions and 155 deletions

View File

@@ -0,0 +1,621 @@
# 🎯 План плавной смены клавиатур (Telegram-style)
## 📊 Анализ проблемы
### Текущая ситуация:
- ✅ Высота клавиатур одинаковая (364.95dp)
- ❌ UI дергается при переключении клавиатур
- ❌ Нет синхронизации анимаций
- ❌ Emoji панель появляется/исчезает резко
### Как работает Telegram:
**Ключевые компоненты:**
1. **AdjustPanLayoutHelper** - координирует анимации клавиатуры
2. **EditTextEmoji.showPopup()** - управляет переключением
3. **ValueAnimator** - анимирует translationY
4. **250ms duration + keyboardInterpolator** (FastOutSlowIn)
**Важные паттерны Telegram:**
```java
// 1. Синхронная анимация - emoji панель двигается вместе с keyboard
animator.setDuration(AdjustPanLayoutHelper.keyboardDuration); // 250ms
animator.setInterpolator(AdjustPanLayoutHelper.keyboardInterpolator);
// 2. TranslationY вместо изменения height
emojiView.setTranslationY(v); // плавно двигаем панель
// 3. Одновременное скрытие keyboard и показ emoji
if (!keyboardVisible && !emojiWasVisible) {
// анимируем появление emoji снизу
ValueAnimator animator = ValueAnimator.ofFloat(emojiPadding, 0);
}
```
## 🎬 Архитектура решения для Compose
### Phase 1: Механизм синхронизации анимаций
#### 1.1 KeyboardTransitionCoordinator
**Цель:** Координировать переходы между клавиатурами
```kotlin
@Composable
fun rememberKeyboardTransitionCoordinator(): KeyboardTransitionCoordinator {
return remember {
KeyboardTransitionCoordinator()
}
}
class KeyboardTransitionCoordinator {
// Состояния перехода
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)
var transitionProgress by mutableFloatStateOf(0f)
// Высоты для расчетов
var keyboardHeight by mutableStateOf(0.dp)
var emojiHeight by mutableStateOf(0.dp)
// Флаги
var isKeyboardVisible by mutableStateOf(false)
var isEmojiVisible by mutableStateOf(false)
// Запуск перехода
fun startTransition(
from: TransitionState,
to: TransitionState,
onComplete: () -> Unit
) {
currentState = to
// Логика анимации
}
}
```
#### 1.2 Compose Animatable для плавности
```kotlin
val emojiOffsetY = remember { Animatable(0f) }
val keyboardOffsetY = remember { Animatable(0f) }
// Telegram-style timing
val transitionSpec = tween<Float>(
durationMillis = 250,
easing = FastOutSlowInEasing // keyboardInterpolator
)
```
### Phase 2: Управление состояниями
#### 2.1 State Machine для переходов
```kotlin
sealed class KeyboardState {
object Closed : KeyboardState()
object SystemKeyboardOpen : KeyboardState()
object EmojiKeyboardOpen : KeyboardState()
data class Transitioning(
val from: KeyboardState,
val to: KeyboardState,
val progress: Float
) : KeyboardState()
}
class KeyboardStateManager {
var currentState by mutableStateOf<KeyboardState>(KeyboardState.Closed)
fun transition(target: KeyboardState) {
when (currentState to target) {
KeyboardState.SystemKeyboardOpen to KeyboardState.EmojiKeyboardOpen -> {
// Keyboard → Emoji
startKeyboardToEmojiTransition()
}
KeyboardState.EmojiKeyboardOpen to KeyboardState.SystemKeyboardOpen -> {
// Emoji → Keyboard
startEmojiToKeyboardTransition()
}
// ... другие переходы
}
}
}
```
### Phase 3: Анимация переходов
#### 3.1 Keyboard → Emoji (самый сложный)
**Проблема:** Системная клавиатура закрывается асинхронно
**Решение Telegram:**
1. **Не ждать закрытия клавиатуры**
2. **Сразу показать emoji панель на месте клавиатуры**
3. **Анимировать emoji снизу вверх, пока keyboard уходит**
```kotlin
@Composable
fun KeyboardToEmojiTransition(
coordinator: KeyboardTransitionCoordinator,
onComplete: () -> Unit
) {
val offsetY = remember { Animatable(coordinator.keyboardHeight.value) }
LaunchedEffect(Unit) {
// Шаг 1: Скрыть клавиатуру (асинхронно)
coordinator.hideSystemKeyboard()
// Шаг 2: Сразу анимировать emoji снизу (250ms)
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
)
)
onComplete()
}
// Emoji панель с offset
Box(
modifier = Modifier
.fillMaxWidth()
.height(coordinator.emojiHeight)
.offset(y = offsetY.value.dp)
) {
OptimizedEmojiPicker(...)
}
}
```
#### 3.2 Emoji → Keyboard
```kotlin
@Composable
fun EmojiToKeyboardTransition(
coordinator: KeyboardTransitionCoordinator,
onComplete: () -> Unit
) {
val emojiOffsetY = remember { Animatable(0f) }
LaunchedEffect(Unit) {
// Шаг 1: Показать клавиатуру (асинхронно)
coordinator.showSystemKeyboard()
// Шаг 2: Одновременно анимировать emoji вниз (250ms)
emojiOffsetY.animateTo(
targetValue = coordinator.emojiHeight.value,
animationSpec = tween(
durationMillis = 250,
easing = FastOutLinearInEasing // быстрее уходит
)
)
onComplete()
}
// Emoji панель уходит вниз
Box(
modifier = Modifier
.fillMaxWidth()
.height(coordinator.emojiHeight)
.offset(y = emojiOffsetY.value.dp)
.alpha(1f - (emojiOffsetY.value / coordinator.emojiHeight.value))
) {
OptimizedEmojiPicker(...)
}
}
```
### Phase 4: Интеграция с IME
#### 4.1 WindowInsets синхронизация
```kotlin
@Composable
fun MessageInputBar() {
val coordinator = rememberKeyboardTransitionCoordinator()
val ime = WindowInsets.ime
val imeHeight = ime.getBottom(LocalDensity.current).toDp()
// Отслеживаем изменения IME
LaunchedEffect(imeHeight) {
coordinator.keyboardHeight = imeHeight
// Если IME закрылась во время показа emoji
if (imeHeight == 0.dp && coordinator.isEmojiVisible) {
// Продолжить показ emoji без дерганья
coordinator.currentState = TransitionState.IDLE
}
}
}
```
#### 4.2 Spacer для резервации места
**Ключевая идея Telegram:** Emoji панель резервирует место клавиатуры
```kotlin
@Composable
fun KeyboardSpacer(
coordinator: KeyboardTransitionCoordinator
) {
val height by animateDpAsState(
targetValue = when {
coordinator.isKeyboardVisible -> coordinator.keyboardHeight
coordinator.isEmojiVisible -> coordinator.emojiHeight
else -> 0.dp
},
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
)
)
Spacer(modifier = Modifier.height(height))
}
```
## 🔧 Детальная имплементация
### Шаг 1: KeyboardTransitionCoordinator.kt
```kotlin
class KeyboardTransitionCoordinator(
private val context: Context
) {
companion object {
const val TRANSITION_DURATION = 250L
val TRANSITION_EASING = FastOutSlowInEasing
}
// Состояние
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 val tag = "KeyboardTransition"
fun requestShowEmoji(
hideKeyboard: () -> Unit,
showEmoji: () -> Unit
) {
Log.d(tag, "🔄 Keyboard → Emoji transition started")
isTransitioning = true
// Telegram паттерн: сначала скрыть клавиатуру
hideKeyboard()
// Через небольшую задержку показать emoji
// (даем системе начать закрытие клавиатуры)
Handler(Looper.getMainLooper()).postDelayed({
showEmoji()
isEmojiVisible = true
isKeyboardVisible = false
isTransitioning = false
Log.d(tag, "✅ Emoji visible, keyboard hidden")
}, 50L)
}
fun requestShowKeyboard(
showKeyboard: () -> Unit,
hideEmoji: () -> Unit
) {
Log.d(tag, "🔄 Emoji → Keyboard transition started")
isTransitioning = true
// Сначала показать клавиатуру
showKeyboard()
// Emoji скрыть после начала анимации
Handler(Looper.getMainLooper()).postDelayed({
hideEmoji()
isEmojiVisible = false
isKeyboardVisible = true
isTransitioning = false
Log.d(tag, "✅ Keyboard visible, emoji hidden")
}, 50L)
}
}
```
### Шаг 2: Обновить MessageInputBar
```kotlin
@Composable
fun MessageInputBar(...) {
val coordinator = rememberKeyboardTransitionCoordinator()
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
// Отслеживание IME
val ime = WindowInsets.ime
val imeHeight = with(LocalDensity.current) {
ime.getBottom(this).toDp()
}
// Обновляем высоту клавиатуры
LaunchedEffect(imeHeight) {
if (imeHeight > 100.dp) {
coordinator.keyboardHeight = imeHeight
coordinator.isKeyboardVisible = true
} else if (imeHeight == 0.dp && !coordinator.isEmojiVisible) {
coordinator.isKeyboardVisible = false
}
Log.d("KeyboardHeight", "IME height: $imeHeight")
}
Column {
// Input field
Row {
TextField(...)
// Emoji button
IconButton(
onClick = {
if (coordinator.isEmojiVisible) {
// Переключить на клавиатуру
coordinator.requestShowKeyboard(
showKeyboard = {
focusRequester.requestFocus()
keyboardController?.show()
},
hideEmoji = {
showEmojiPicker = false
}
)
} else {
// Переключить на emoji
coordinator.requestShowEmoji(
hideKeyboard = {
keyboardController?.hide()
},
showEmoji = {
showEmojiPicker = true
coordinator.emojiHeight = coordinator.keyboardHeight
}
)
}
}
) {
Icon(
imageVector = if (coordinator.isEmojiVisible) {
Icons.Default.Keyboard
} else {
Icons.Default.EmojiEmotions
}
)
}
}
// Emoji picker с анимацией
AnimatedKeyboardTransition(
coordinator = coordinator,
showEmojiPicker = showEmojiPicker
)
}
}
```
### Шаг 3: AnimatedKeyboardTransition.kt
```kotlin
@Composable
fun AnimatedKeyboardTransition(
coordinator: KeyboardTransitionCoordinator,
showEmojiPicker: Boolean
) {
val offsetY = remember { Animatable(0f) }
val alpha = remember { Animatable(0f) }
LaunchedEffect(showEmojiPicker) {
if (showEmojiPicker) {
// Показать emoji
launch {
offsetY.snapTo(coordinator.emojiHeight.value)
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
)
)
}
launch {
alpha.snapTo(0f)
alpha.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 200)
)
}
} else {
// Скрыть emoji
launch {
offsetY.animateTo(
targetValue = coordinator.emojiHeight.value,
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
}
launch {
alpha.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 150)
)
}
}
}
if (showEmojiPicker || offsetY.value > 0f) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(coordinator.emojiHeight)
.offset(y = offsetY.value.dp)
.alpha(alpha.value)
) {
OptimizedEmojiPicker(
keyboardHeight = coordinator.emojiHeight,
...
)
}
}
}
```
## 📈 Этапы внедрения
### Этап 1: Базовая инфраструктура (1-2 часа)
- [ ] Создать `KeyboardTransitionCoordinator.kt`
- [ ] Добавить state management для переходов
- [ ] Настроить логирование для отладки
### Этап 2: Анимации (2-3 часа)
- [ ] Создать `AnimatedKeyboardTransition.kt`
- [ ] Реализовать Keyboard → Emoji переход
- [ ] Реализовать Emoji → Keyboard переход
- [ ] Добавить fade анимацию для плавности
### Этап 3: Интеграция (1-2 часа)
- [ ] Обновить `MessageInputBar` с coordinator
- [ ] Интегрировать с `OptimizedEmojiPicker`
- [ ] Добавить `KeyboardSpacer` для резервации места
### Этап 4: Полировка (1-2 часа)
- [ ] Синхронизация с IME events
- [ ] Обработка edge cases (поворот экрана, многозадачность)
- [ ] Убрать дерганья при быстрых переключениях
- [ ] Тестирование на разных устройствах
### Этап 5: Оптимизация (1 час)
- [ ] Убрать избыточные recomposition
- [ ] Добавить `remember` где нужно
- [ ] Проверить производительность
- [ ] Удалить debug логи (опционально)
## 🎨 Ключевые паттерны Telegram
### 1. **Немедленное резервирование места**
```kotlin
// Telegram сразу резервирует место для emoji панели
emojiPadding = currentHeight
sizeNotifierLayout.requestLayout()
```
### 2. **TranslationY вместо show/hide**
```kotlin
// Анимация позиции, а не visibility
emojiView.setTranslationY(v)
```
### 3. **Синхронные анимации**
```kotlin
// Все анимации используют одинаковый timing
animator.setDuration(AdjustPanLayoutHelper.keyboardDuration); // 250ms
animator.setInterpolator(AdjustPanLayoutHelper.keyboardInterpolator);
```
### 4. **Плавный alpha для emoji**
```kotlin
// Fade in/out для естественности
emojiViewAlpha = 1f - v / (float) emojiPadding;
emojiView.setAlpha(emojiViewAlpha);
```
## 🐛 Возможные проблемы и решения
### Проблема 1: IME закрывается слишком медленно
**Решение:** Не ждать закрытия, показать emoji сразу
```kotlin
// Telegram паттерн
if (!keyboardVisible && !emojiWasVisible) {
// Анимировать emoji снизу, пока keyboard закрывается
}
```
### Проблема 2: UI дергается при переключении
**Решение:** Резервировать место спейсером
```kotlin
Spacer(modifier = Modifier.height(max(keyboardHeight, emojiHeight)))
```
### Проблема 3: Разные высоты на landscape/portrait
**Решение:** Сохранять отдельно для каждой ориентации
```kotlin
val keyboardHeight = if (isLandscape) keyboardHeightLand else keyboardHeightPort
```
### Проблема 4: Быстрые переключения
**Решение:** Отменять предыдущие анимации
```kotlin
LaunchedEffect(showEmojiPicker) {
offsetY.stop() // Остановить текущую анимацию
// Начать новую
}
```
## 📊 Ожидаемый результат
### До:
- ❌ UI дергается
- ❌ Резкое появление/исчезновение
- ❌ Несинхронные анимации
- ❌ Пустое место при переключении
### После:
- ✅ Плавный переход за 250ms
- ✅ Синхронизированные анимации
- ✅ Telegram-style UX
- ✅ Нет дерганий UI
- ✅ Резервированное место
## 🚀 Готовы начинать?
План готов! Начнем с создания `KeyboardTransitionCoordinator.kt` - это основа всей системы. После этого добавим анимации и интегрируем в `MessageInputBar`.
Telegram решает эту задачу через:
1. **ValueAnimator** с 250ms и FastOutSlowIn
2. **TranslationY** для плавного движения
3. **Немедленное резервирование места**
4. **Синхронизация всех анимаций**
Готов приступить к реализации! 🎯

View File

@@ -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()
}
}
}

View File

@@ -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() }
}

View File

@@ -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 ->
// 🔥 НЕ открываем клавиатуру если 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: Немедленно фокусируем и открываем клавиатуру
// 🔥 ИСПОЛЬЗУЕМ coordinator.isEmojiVisible вместо showEmojiPicker для более точного состояния
if (coordinator.isEmojiVisible) {
// ========== EMOJI → KEYBOARD ==========
android.util.Log.d("EmojiPicker", "🔄 Switching: Emoji → Keyboard")
coordinator.requestShowKeyboard(
showKeyboard = {
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)")
android.util.Log.d("EmojiPicker", "📱 Keyboard show requested")
}
// Метод 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")
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
} else {
android.util.Log.d("EmojiPicker", " [5] ✅ Keyboard opened successfully in ${elapsed}ms!")
},
hideEmoji = {
onToggleEmojiPicker(false)
android.util.Log.d("EmojiPicker", "😊 Emoji panel hidden")
}
}, 200)
} ?: android.util.Log.e("EmojiPicker", " ❌ ERROR: editTextView is null!")
)
} else {
// ========== ОТКРЫВАЕМ EMOJI → ЗАКРЫВАЕМ КЛАВИАТУРУ ==========
android.util.Log.d("EmojiPicker", "😊 Action: OPENING emoji → CLOSING keyboard")
// Шаг 1: Скрываем клавиатуру
// ========== KEYBOARD → EMOJI ==========
android.util.Log.d("EmojiPicker", "🔄 Switching: Keyboard → Emoji")
coordinator.requestShowEmoji(
hideKeyboard = {
imm.hideSoftInputFromWindow(view.windowToken, 0)
android.util.Log.d("EmojiPicker", " [1] Keyboard hide requested")
// Шаг 2: Небольшая задержка для плавности
view.postDelayed({
// Шаг 3: Открываем emoji панель
android.util.Log.d("EmojiPicker", "⌨️ Keyboard hide requested")
},
showEmoji = {
onToggleEmojiPicker(true)
android.util.Log.d("EmojiPicker", " [2] ✅ Emoji panel opened (50ms delay)")
}, 50)
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) {
// Новый оптимизированный пикер автоматически управляет анимациями
AnimatedKeyboardTransition(
coordinator = coordinator,
showEmojiPicker = showEmojiPicker
) {
OptimizedEmojiPicker(
isVisible = showEmojiPicker && !isKeyboardVisible,
isVisible = true, // Видимость контролирует AnimatedKeyboardTransition
isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji ->
onValueChange(value + emoji)
},
onClose = {
// 🔥 Используем toggleEmojiPicker() - он закроет панель и откроет клавиатуру
// Используем coordinator для плавного перехода
toggleEmojiPicker()
},
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
)
}
} // End of if (!isBlocked) for emoji picker
}
}

View File

@@ -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)
}

View File

@@ -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
)
),
modifier = modifier
) {
// 🎨 Hardware layer для анимаций (GPU ускорение - как в Telegram)
Box(
modifier = Modifier.graphicsLayer {
// Используем hardware layer только во время анимации
if (transition.isRunning) {
this.alpha = 1f
}
}
) {
// 🔥 Рендерим контент напрямую без AnimatedVisibility
// Анимация теперь управляется AnimatedKeyboardTransition
EmojiPickerContent(
isDarkTheme = isDarkTheme,
onEmojiSelected = onEmojiSelected,
keyboardHeight = savedKeyboardHeight
keyboardHeight = savedKeyboardHeight,
modifier = modifier
)
}
}
}
/**
@@ -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)