feat: Update menu icon color for improved visibility in ChatsListScreen

This commit is contained in:
k1ngsterr1
2026-01-17 21:03:19 +05:00
parent 569aa34432
commit c9136ed499
10 changed files with 939 additions and 879 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. **Синхронизация всех анимаций**
Готов приступить к реализации! 🎯