feat: add emoji picker with smooth transition and keyboard height management
This commit is contained in:
@@ -43,10 +43,11 @@ fun AnimatedKeyboardTransition(
|
||||
// 📊 Последнее залогированное значение alpha (для фильтрации)
|
||||
val lastLoggedAlpha = remember { mutableStateOf(-1f) }
|
||||
|
||||
// 🔥 Клавиатура достигла ПОЛНОЙ высоты (разница не более 3dp)
|
||||
val isKeyboardFullHeight = coordinator.emojiHeight > 0.dp &&
|
||||
coordinator.keyboardHeight >= coordinator.emojiHeight - 3.dp
|
||||
|
||||
// 🔥 Клавиатура достигла высоты почти равной emoji (разница < 2dp)
|
||||
// Это гарантирует плавный переход без прыжков и ререндеров
|
||||
val isKeyboardNearFullHeight = coordinator.emojiHeight > 0.dp &&
|
||||
coordinator.keyboardHeight >= coordinator.emojiHeight - 2.dp
|
||||
|
||||
// Логика перехода
|
||||
if (showEmojiPicker && !wasEmojiShown) {
|
||||
wasEmojiShown = true
|
||||
@@ -55,10 +56,10 @@ fun AnimatedKeyboardTransition(
|
||||
// Emoji закрылся после того как был открыт = переход emoji→keyboard
|
||||
isTransitioningToKeyboard = true
|
||||
}
|
||||
|
||||
// 🔥 КЛЮЧЕВОЕ: Сбрасываем флаг БЕЗ задержки когда клавиатура полностью открылась
|
||||
// Проверяем прямо в composition для мгновенной реакции
|
||||
if (isKeyboardFullHeight && isTransitioningToKeyboard) {
|
||||
|
||||
// 🔥 КЛЮЧЕВОЕ: Сбрасываем флаг когда клавиатура достигла высоты близкой к emoji
|
||||
// Это предотвращает прыжок инпута при переходе
|
||||
if (isKeyboardNearFullHeight && isTransitioningToKeyboard && !showEmojiPicker) {
|
||||
isTransitioningToKeyboard = false
|
||||
wasEmojiShown = false
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ class KeyboardTransitionCoordinator {
|
||||
emojiHeight = maxKeyboardHeight
|
||||
}
|
||||
|
||||
// 🔥 КРИТИЧНО: Устанавливаем isEmojiBoxVisible СРАЗУ, ДО showEmoji()!
|
||||
// Это отключает imePadding на ПЕРВОМ же рендере, предотвращая "прыжок"
|
||||
isEmojiBoxVisible = true
|
||||
|
||||
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
|
||||
showEmoji()
|
||||
isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji!
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
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.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||
import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
||||
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||
@@ -210,16 +212,20 @@ fun ImageEditorScreen(
|
||||
|
||||
// Отслеживание высоты клавиатуры
|
||||
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 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
|
||||
|
||||
Log.d(TAG, "toggleEmojiPicker: isEmojiVisible=${coordinator.isEmojiVisible}, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, showEmojiPicker=$showEmojiPicker")
|
||||
|
||||
if (coordinator.isEmojiVisible) {
|
||||
// EMOJI → KEYBOARD
|
||||
Log.d(TAG, "TRANSITION: EMOJI → KEYBOARD")
|
||||
coordinator.requestShowKeyboard(
|
||||
showKeyboard = {
|
||||
editTextView?.let { editText ->
|
||||
editText.requestFocus()
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
},
|
||||
hideEmoji = { showEmojiPicker = false }
|
||||
)
|
||||
} else {
|
||||
// KEYBOARD → EMOJI
|
||||
Log.d(TAG, "TRANSITION: KEYBOARD → EMOJI")
|
||||
coordinator.requestShowEmoji(
|
||||
hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) },
|
||||
showEmoji = { showEmojiPicker = true }
|
||||
@@ -513,42 +523,22 @@ fun ImageEditorScreen(
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📝 CAPTION INPUT + EMOJI PICKER - Telegram style
|
||||
// Прячется с анимацией когда открыты инструменты (рисовалка, поворот и т.д.)
|
||||
// Использует imePadding + AnimatedKeyboardTransition как в ChatDetailInput
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔥 TELEGRAM-STYLE FIX: Единый spacer вместо imePadding
|
||||
// Это ПОЛНОСТЬЮ устраняет прыжки при переходе 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
|
||||
// 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji НЕ показан
|
||||
val shouldUseImePadding = !coordinator.isEmojiBoxVisible
|
||||
|
||||
// Когда клавиатура/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(
|
||||
visible = showCaptionInput && currentTool == EditorTool.NONE,
|
||||
@@ -562,13 +552,15 @@ fun ImageEditorScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = bottomPaddingForCaption)
|
||||
// 🔥 БЕЗ imePadding! Всё контролируется через spacer ниже
|
||||
// 🔥 imePadding ТОЛЬКО когда emoji НЕ показан
|
||||
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
|
||||
) {
|
||||
TelegramCaptionBar(
|
||||
caption = caption,
|
||||
onCaptionChange = { caption = it },
|
||||
isSaving = isSaving,
|
||||
isKeyboardVisible = isImeActuallyOpen || showEmojiPicker,
|
||||
// 🔥 Добавляем isEmojiBoxVisible чтобы во время перехода emoji→keyboard инпут не сжимался
|
||||
isKeyboardVisible = isKeyboardVisible || showEmojiPicker || coordinator.isEmojiBoxVisible,
|
||||
showEmojiPicker = showEmojiPicker,
|
||||
onToggleEmojiPicker = { toggleEmojiPicker() },
|
||||
onEditTextViewCreated = { editTextView = it },
|
||||
@@ -590,30 +582,20 @@ fun ImageEditorScreen(
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔥 UNIFIED SPACER: Один Box для keyboard И emoji
|
||||
// Высота = imeHeight когда keyboard, = emojiHeight когда emoji
|
||||
// 🔥 EMOJI PICKER - используем AnimatedKeyboardTransition
|
||||
// Точно как в ChatDetailInput!
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (needsSpacer) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(spacerHeight)
|
||||
) {
|
||||
// Emoji picker рендерится ВНУТРИ spacer'а с fade анимацией
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = showEmojiPicker,
|
||||
enter = fadeIn(animationSpec = tween(200)),
|
||||
exit = fadeOut(animationSpec = tween(200))
|
||||
) {
|
||||
OptimizedEmojiPicker(
|
||||
isVisible = true,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = { emoji -> caption = caption + emoji },
|
||||
onClose = { toggleEmojiPicker() },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedKeyboardTransition(
|
||||
coordinator = coordinator,
|
||||
showEmojiPicker = showEmojiPicker
|
||||
) {
|
||||
OptimizedEmojiPicker(
|
||||
isVisible = true,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = { emoji -> caption = caption + emoji },
|
||||
onClose = { toggleEmojiPicker() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package com.rosetta.messenger.ui.chats.components
|
||||
import android.Manifest
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.*
|
||||
@@ -25,9 +27,11 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -59,7 +64,13 @@ import coil.request.ImageRequest
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
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.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.roundToInt
|
||||
@@ -1383,6 +1394,11 @@ private fun formatDuration(durationMs: Long): String {
|
||||
|
||||
/**
|
||||
* Экран предпросмотра фото с caption и кнопкой отправки (как в Telegram)
|
||||
* С плавной анимацией перехода между клавиатурой и emoji picker
|
||||
*
|
||||
* Использует тот же подход что ChatDetailInput:
|
||||
* - imePadding() применяется ТОЛЬКО когда emoji НЕ показан
|
||||
* - AnimatedKeyboardTransition управляет emoji picker
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -1395,98 +1411,270 @@ fun PhotoPreviewWithCaptionScreen(
|
||||
isDarkTheme: Boolean
|
||||
) {
|
||||
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(
|
||||
color = backgroundColor,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.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.fillMaxSize()) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📸 FULLSCREEN PHOTO - не реагирует на клавиатуру
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(imageUri)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎛️ TOP BAR - Transparent overlay
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
.align(Alignment.TopCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUri)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
|
||||
// Caption input
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = caption,
|
||||
onValueChange = onCaptionChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = {
|
||||
Text(
|
||||
"Add a caption...",
|
||||
color = textColor.copy(alpha = 0.5f)
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
showEmojiPicker = false
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
focusManager.clearFocus()
|
||||
onDismiss()
|
||||
},
|
||||
maxLines = 3,
|
||||
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)
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.Send,
|
||||
contentDescription = "Send",
|
||||
TablerIcons.X,
|
||||
contentDescription = "Close",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(28.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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user