Files
mobile-android/docs/SMOOTH_KEYBOARD_TRANSITION_PLAN.md

622 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🎯 План плавной смены клавиатур (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. **Синхронизация всех анимаций**
Готов приступить к реализации! 🎯