feat: add emoji picker with smooth transition and keyboard height management

This commit is contained in:
k1ngsterr1
2026-02-02 23:33:24 +05:00
parent dc23ba9d36
commit eb96d269f6
4 changed files with 318 additions and 143 deletions

View File

@@ -43,9 +43,10 @@ fun AnimatedKeyboardTransition(
// 📊 Последнее залогированное значение alpha (для фильтрации) // 📊 Последнее залогированное значение alpha (для фильтрации)
val lastLoggedAlpha = remember { mutableStateOf(-1f) } val lastLoggedAlpha = remember { mutableStateOf(-1f) }
// 🔥 Клавиатура достигла ПОЛНОЙ высоты (разница не более 3dp) // 🔥 Клавиатура достигла высоты почти равной emoji (разница < 2dp)
val isKeyboardFullHeight = coordinator.emojiHeight > 0.dp && // Это гарантирует плавный переход без прыжков и ререндеров
coordinator.keyboardHeight >= coordinator.emojiHeight - 3.dp val isKeyboardNearFullHeight = coordinator.emojiHeight > 0.dp &&
coordinator.keyboardHeight >= coordinator.emojiHeight - 2.dp
// Логика перехода // Логика перехода
if (showEmojiPicker && !wasEmojiShown) { if (showEmojiPicker && !wasEmojiShown) {
@@ -56,9 +57,9 @@ fun AnimatedKeyboardTransition(
isTransitioningToKeyboard = true isTransitioningToKeyboard = true
} }
// 🔥 КЛЮЧЕВОЕ: Сбрасываем флаг БЕЗ задержки когда клавиатура полностью открылась // 🔥 КЛЮЧЕВОЕ: Сбрасываем флаг когда клавиатура достигла высоты близкой к emoji
// Проверяем прямо в composition для мгновенной реакции // Это предотвращает прыжок инпута при переходе
if (isKeyboardFullHeight && isTransitioningToKeyboard) { if (isKeyboardNearFullHeight && isTransitioningToKeyboard && !showEmojiPicker) {
isTransitioningToKeyboard = false isTransitioningToKeyboard = false
wasEmojiShown = false wasEmojiShown = false
} }

View File

@@ -89,6 +89,10 @@ class KeyboardTransitionCoordinator {
emojiHeight = maxKeyboardHeight emojiHeight = maxKeyboardHeight
} }
// 🔥 КРИТИЧНО: Устанавливаем isEmojiBoxVisible СРАЗУ, ДО showEmoji()!
// Это отключает imePadding на ПЕРВОМ же рендере, предотвращая "прыжок"
isEmojiBoxVisible = true
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры // 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
showEmoji() showEmoji()
isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji! isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji!

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Log
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@@ -50,6 +51,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
import com.rosetta.messenger.ui.components.AppleEmojiTextField import com.rosetta.messenger.ui.components.AppleEmojiTextField
import com.rosetta.messenger.ui.components.KeyboardHeightProvider import com.rosetta.messenger.ui.components.KeyboardHeightProvider
@@ -210,16 +212,20 @@ fun ImageEditorScreen(
// Отслеживание высоты клавиатуры // Отслеживание высоты клавиатуры
val imeInsets = WindowInsets.ime val imeInsets = WindowInsets.ime
var isKeyboardVisible by remember { mutableStateOf(false) }
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) } var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
// Update coordinator through snapshotFlow // Update coordinator through snapshotFlow
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight -> snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight ->
isKeyboardVisible = currentImeHeight > 50.dp
coordinator.updateKeyboardHeight(currentImeHeight) coordinator.updateKeyboardHeight(currentImeHeight)
if (currentImeHeight > 100.dp) { if (currentImeHeight > 100.dp) {
coordinator.syncHeights() coordinator.syncHeights()
lastStableKeyboardHeight = currentImeHeight lastStableKeyboardHeight = currentImeHeight
} }
// 📊 Log IME height changes
Log.d(TAG, "IME height: ${currentImeHeight.value}dp, isKeyboardVisible: $isKeyboardVisible, emojiHeight: ${coordinator.emojiHeight.value}dp, isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}")
} }
} }
@@ -248,19 +254,23 @@ fun ImageEditorScreen(
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
Log.d(TAG, "toggleEmojiPicker: isEmojiVisible=${coordinator.isEmojiVisible}, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, showEmojiPicker=$showEmojiPicker")
if (coordinator.isEmojiVisible) { if (coordinator.isEmojiVisible) {
// EMOJI → KEYBOARD // EMOJI → KEYBOARD
Log.d(TAG, "TRANSITION: EMOJI → KEYBOARD")
coordinator.requestShowKeyboard( coordinator.requestShowKeyboard(
showKeyboard = { showKeyboard = {
editTextView?.let { editText -> editTextView?.let { editText ->
editText.requestFocus() editText.requestFocus()
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
} }
}, },
hideEmoji = { showEmojiPicker = false } hideEmoji = { showEmojiPicker = false }
) )
} else { } else {
// KEYBOARD → EMOJI // KEYBOARD → EMOJI
Log.d(TAG, "TRANSITION: KEYBOARD → EMOJI")
coordinator.requestShowEmoji( coordinator.requestShowEmoji(
hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) }, hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) },
showEmoji = { showEmojiPicker = true } showEmoji = { showEmojiPicker = true }
@@ -513,42 +523,22 @@ fun ImageEditorScreen(
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 📝 CAPTION INPUT + EMOJI PICKER - Telegram style // 📝 CAPTION INPUT + EMOJI PICKER - Telegram style
// Прячется с анимацией когда открыты инструменты (рисовалка, поворот и т.д.) // Использует imePadding + AnimatedKeyboardTransition как в ChatDetailInput
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════ // 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji НЕ показан
// 🔥 TELEGRAM-STYLE FIX: Единый spacer вместо imePadding val shouldUseImePadding = !coordinator.isEmojiBoxVisible
// Это ПОЛНОСТЬЮ устраняет прыжки при переходе keyboard ↔ emoji
// ═══════════════════════════════════════════════════════════
val currentImeHeight = with(density) { WindowInsets.ime.getBottom(density).toDp() }
val isImeActuallyOpen = currentImeHeight > 50.dp
// 🔥 КЛЮЧЕВОЕ: Единая высота spacer'а - ИЛИ keyboard ИЛИ emoji
// Когда keyboard открыта: spacerHeight = imeHeight
// Когда emoji открыта: spacerHeight = emojiHeight
// Это даёт плавный переход без прыжков!
// Fallback высота emoji на случай если coordinator.emojiHeight = 0
val effectiveEmojiHeight = when {
coordinator.emojiHeight > 0.dp -> coordinator.emojiHeight
lastStableKeyboardHeight > 0.dp -> lastStableKeyboardHeight
else -> 300.dp // Минимальная fallback высота
}
val spacerHeight = when {
isImeActuallyOpen -> currentImeHeight // Keyboard открыта - её высота
showEmojiPicker || coordinator.isEmojiBoxVisible -> effectiveEmojiHeight // Emoji открыта
else -> 0.dp // Ничего не открыто
}
// Нужен ли spacer вообще
val needsSpacer = spacerHeight > 0.dp
// 🔥 Обновляем coordinator для правильной работы toolbar
coordinator.isEmojiBoxVisible = showEmojiPicker
// Когда клавиатура/emoji закрыты - добавляем отступ снизу для toolbar (~100dp) // Когда клавиатура/emoji закрыты - добавляем отступ снизу для toolbar (~100dp)
val bottomPaddingForCaption = if (!needsSpacer) 100.dp else 0.dp // 🔥 Анимируем отступ для плавного перехода
val bottomPaddingForCaption by animateDpAsState(
targetValue = if (!isKeyboardVisible && !coordinator.isEmojiBoxVisible) 100.dp else 0.dp,
animationSpec = tween(200, easing = FastOutSlowInEasing),
label = "bottomPadding"
)
// 📊 Log render state
Log.d(TAG, "RENDER: showEmoji=$showEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp, bottomPadding=${bottomPaddingForCaption.value}dp")
AnimatedVisibility( AnimatedVisibility(
visible = showCaptionInput && currentTool == EditorTool.NONE, visible = showCaptionInput && currentTool == EditorTool.NONE,
@@ -562,13 +552,15 @@ fun ImageEditorScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = bottomPaddingForCaption) .padding(bottom = bottomPaddingForCaption)
// 🔥 БЕЗ imePadding! Всё контролируется через spacer ниже // 🔥 imePadding ТОЛЬКО когда emoji НЕ показан
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
) { ) {
TelegramCaptionBar( TelegramCaptionBar(
caption = caption, caption = caption,
onCaptionChange = { caption = it }, onCaptionChange = { caption = it },
isSaving = isSaving, isSaving = isSaving,
isKeyboardVisible = isImeActuallyOpen || showEmojiPicker, // 🔥 Добавляем isEmojiBoxVisible чтобы во время перехода emoji→keyboard инпут не сжимался
isKeyboardVisible = isKeyboardVisible || showEmojiPicker || coordinator.isEmojiBoxVisible,
showEmojiPicker = showEmojiPicker, showEmojiPicker = showEmojiPicker,
onToggleEmojiPicker = { toggleEmojiPicker() }, onToggleEmojiPicker = { toggleEmojiPicker() },
onEditTextViewCreated = { editTextView = it }, onEditTextViewCreated = { editTextView = it },
@@ -590,33 +582,23 @@ fun ImageEditorScreen(
) )
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 🔥 UNIFIED SPACER: Один Box для keyboard И emoji // 🔥 EMOJI PICKER - используем AnimatedKeyboardTransition
// Высота = imeHeight когда keyboard, = emojiHeight когда emoji // Точно как в ChatDetailInput!
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (needsSpacer) { AnimatedKeyboardTransition(
Box( coordinator = coordinator,
modifier = Modifier showEmojiPicker = showEmojiPicker
.fillMaxWidth()
.height(spacerHeight)
) {
// Emoji picker рендерится ВНУТРИ spacer'а с fade анимацией
androidx.compose.animation.AnimatedVisibility(
visible = showEmojiPicker,
enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(200))
) { ) {
OptimizedEmojiPicker( OptimizedEmojiPicker(
isVisible = true, isVisible = true,
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji -> caption = caption + emoji }, onEmojiSelected = { emoji -> caption = caption + emoji },
onClose = { toggleEmojiPicker() }, onClose = { toggleEmojiPicker() },
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxWidth()
) )
} }
} }
} }
}
}
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 🛠️ TOOLBAR - показывается только когда клавиатура и emoji ЗАКРЫТЫ // 🛠️ TOOLBAR - показывается только когда клавиатура и emoji ЗАКРЫТЫ

View File

@@ -3,11 +3,13 @@ package com.rosetta.messenger.ui.chats.components
import android.Manifest import android.Manifest
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.util.Log
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.* import androidx.compose.animation.*
@@ -25,9 +27,11 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@@ -36,6 +40,7 @@ import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -59,7 +64,13 @@ import coil.request.ImageRequest
import compose.icons.TablerIcons import compose.icons.TablerIcons
import compose.icons.tablericons.* import compose.icons.tablericons.*
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.components.AppleEmojiTextField
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -1383,6 +1394,11 @@ private fun formatDuration(durationMs: Long): String {
/** /**
* Экран предпросмотра фото с caption и кнопкой отправки (как в Telegram) * Экран предпросмотра фото с caption и кнопкой отправки (как в Telegram)
* С плавной анимацией перехода между клавиатурой и emoji picker
*
* Использует тот же подход что ChatDetailInput:
* - imePadding() применяется ТОЛЬКО когда emoji НЕ показан
* - AnimatedKeyboardTransition управляет emoji picker
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -1395,44 +1411,115 @@ fun PhotoPreviewWithCaptionScreen(
isDarkTheme: Boolean isDarkTheme: Boolean
) { ) {
val backgroundColor = if (isDarkTheme) Color.Black else Color.White val backgroundColor = if (isDarkTheme) Color.Black else Color.White
val textColor = if (isDarkTheme) Color.White else Color.Black
val context = LocalContext.current
val view = LocalView.current
val focusManager = LocalFocusManager.current
val density = LocalDensity.current
// ═══════════════════════════════════════════════════════════════
// 😀 EMOJI PICKER STATE
// ═══════════════════════════════════════════════════════════════
var showEmojiPicker by remember { mutableStateOf(false) }
val coordinator = rememberKeyboardTransitionCoordinator()
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
var lastToggleTime by remember { mutableLongStateOf(0L) }
val toggleCooldownMs = 500L
// Отслеживание высоты клавиатуры
val imeInsets = WindowInsets.ime
var isKeyboardVisible by remember { mutableStateOf(false) }
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
// Update coordinator through snapshotFlow
LaunchedEffect(Unit) {
snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight ->
isKeyboardVisible = currentImeHeight > 50.dp
coordinator.updateKeyboardHeight(currentImeHeight)
if (currentImeHeight > 100.dp) {
coordinator.syncHeights()
lastStableKeyboardHeight = currentImeHeight
}
Log.d("PhotoPreview", "IME height: ${currentImeHeight.value}dp, isKeyboardVisible: $isKeyboardVisible, emojiHeight: ${coordinator.emojiHeight.value}dp, isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}")
}
}
// Load saved keyboard height
LaunchedEffect(Unit) {
KeyboardHeightProvider.getSavedKeyboardHeight(context)
}
// Save keyboard height when stable
LaunchedEffect(isKeyboardVisible, showEmojiPicker) {
if (isKeyboardVisible && !showEmojiPicker) {
delay(350)
if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) {
val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() }
KeyboardHeightProvider.saveKeyboardHeight(context, heightPx)
}
}
}
// Toggle emoji picker function
fun toggleEmojiPicker() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastToggleTime < toggleCooldownMs) {
Log.d("PhotoPreview", "Toggle blocked by cooldown")
return
}
lastToggleTime = currentTime
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (coordinator.isEmojiVisible) {
// EMOJI → KEYBOARD
Log.d("PhotoPreview", "TOGGLE: Emoji → Keyboard")
coordinator.requestShowKeyboard(
showKeyboard = {
Log.d("PhotoPreview", "Showing keyboard...")
editTextView?.let { editText ->
editText.requestFocus()
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
},
hideEmoji = {
Log.d("PhotoPreview", "Hiding emoji picker")
showEmojiPicker = false
}
)
} else {
// KEYBOARD → EMOJI
Log.d("PhotoPreview", "TOGGLE: Keyboard → Emoji")
coordinator.requestShowEmoji(
hideKeyboard = {
Log.d("PhotoPreview", "Hiding keyboard...")
imm.hideSoftInputFromWindow(view.windowToken, 0)
},
showEmoji = {
Log.d("PhotoPreview", "Showing emoji picker")
showEmojiPicker = true
}
)
}
}
// 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji box НЕ виден
val shouldUseImePadding = !coordinator.isEmojiBoxVisible
val shouldAddNavBarPadding = !isKeyboardVisible && !coordinator.isEmojiBoxVisible
// Логируем состояние при каждой рекомпозиции
Log.d("PhotoPreview", "RENDER: showEmoji=$showEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp")
Surface( Surface(
color = backgroundColor, color = backgroundColor,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Column( Box(modifier = Modifier.fillMaxSize()) {
modifier = Modifier // ═══════════════════════════════════════════════════════════
.fillMaxSize() // 📸 FULLSCREEN PHOTO - не реагирует на клавиатуру
.systemBarsPadding() // ═══════════════════════════════════════════════════════════
.imePadding()
) {
// Top bar with close button
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
tint = textColor
)
}
}
// Image preview
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(context)
.data(imageUri) .data(imageUri)
.crossfade(true) .crossfade(true)
.build(), .build(),
@@ -1440,53 +1527,154 @@ fun PhotoPreviewWithCaptionScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
}
// Caption input // ═══════════════════════════════════════════════════════════
Row( // 🎛️ TOP BAR - Transparent overlay
// ═══════════════════════════════════════════════════════════
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .align(Alignment.TopCenter)
verticalAlignment = Alignment.CenterVertically, .background(
horizontalArrangement = Arrangement.spacedBy(8.dp) Brush.verticalGradient(
colors = listOf(
Color.Black.copy(alpha = 0.5f),
Color.Transparent
)
)
)
.statusBarsPadding()
.padding(horizontal = 4.dp, vertical = 8.dp)
) { ) {
OutlinedTextField( IconButton(
value = caption, onClick = {
onValueChange = onCaptionChange, showEmojiPicker = false
modifier = Modifier.weight(1f), val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
placeholder = { imm.hideSoftInputFromWindow(view.windowToken, 0)
Text( focusManager.clearFocus()
"Add a caption...", onDismiss()
color = textColor.copy(alpha = 0.5f)
)
}, },
maxLines = 3, modifier = Modifier.align(Alignment.CenterStart)
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedTextColor = textColor,
unfocusedTextColor = textColor,
cursorColor = PrimaryBlue,
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = textColor.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(20.dp)
)
// Send button
FloatingActionButton(
onClick = onSend,
containerColor = PrimaryBlue,
modifier = Modifier.size(56.dp)
) { ) {
Icon( Icon(
imageVector = TablerIcons.Send, TablerIcons.X,
contentDescription = "Send", contentDescription = "Close",
tint = Color.White, tint = Color.White,
modifier = Modifier.size(24.dp) modifier = Modifier.size(28.dp)
) )
} }
} }
Spacer(modifier = Modifier.height(8.dp)) // ═══════════════════════════════════════════════════════════
// 📝 CAPTION INPUT + EMOJI PICKER
// Как в ChatDetailInput: imePadding + AnimatedKeyboardTransition
// ═══════════════════════════════════════════════════════════
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
// 🔥 imePadding ТОЛЬКО когда emoji НЕ показан
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
) {
// Caption bar
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Black.copy(alpha = 0.75f))
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
bottom = if (isKeyboardVisible || coordinator.isEmojiBoxVisible) 10.dp else 16.dp
)
.then(if (shouldAddNavBarPadding) Modifier.navigationBarsPadding() else Modifier)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// Emoji/Keyboard toggle button
IconButton(
onClick = { toggleEmojiPicker() },
modifier = Modifier.size(32.dp)
) {
Crossfade(
targetState = showEmojiPicker,
animationSpec = tween(150),
label = "emojiIcon"
) { isEmoji ->
Icon(
if (isEmoji) TablerIcons.Keyboard else TablerIcons.MoodSmile,
contentDescription = if (isEmoji) "Keyboard" else "Emoji",
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier.size(26.dp)
)
}
}
// Caption text field
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = 24.dp, max = 100.dp)
) {
AppleEmojiTextField(
value = caption,
onValueChange = onCaptionChange,
textColor = Color.White,
textSize = 16f,
hint = "Add a caption...",
hintColor = Color.White.copy(alpha = 0.5f),
modifier = Modifier.fillMaxWidth(),
requestFocus = false,
onViewCreated = { v -> editTextView = v },
onFocusChanged = { hasFocus ->
if (hasFocus && showEmojiPicker) {
toggleEmojiPicker()
}
}
)
}
// Send button
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(PrimaryBlue)
.clickable { onSend() },
contentAlignment = Alignment.Center
) {
Icon(
TablerIcons.Send,
contentDescription = "Send",
tint = Color.White,
modifier = Modifier
.size(22.dp)
.offset(x = 1.dp)
)
}
}
}
// ═══════════════════════════════════════════════════════════
// 🔥 EMOJI PICKER - используем AnimatedKeyboardTransition
// Точно как в ChatDetailInput!
// ═══════════════════════════════════════════════════════════
AnimatedKeyboardTransition(
coordinator = coordinator,
showEmojiPicker = showEmojiPicker
) {
OptimizedEmojiPicker(
isVisible = true,
isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji -> onCaptionChange(caption + emoji) },
onClose = { toggleEmojiPicker() },
modifier = Modifier.fillMaxWidth()
)
}
}
} }
} }
} }