feat: add emoji picker with smooth transition and keyboard height management
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -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 ЗАКРЫТЫ
|
||||||
|
|||||||
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user