fix: adjust padding and status bar colors in ImageEditorScreen for improved UI experience

This commit is contained in:
k1ngsterr1
2026-02-03 01:56:08 +05:00
parent 5c5c5e45ee
commit 7d90a9d744
3 changed files with 267 additions and 154 deletions

View File

@@ -84,6 +84,8 @@ import android.provider.MediaStore
import androidx.core.content.FileProvider
import java.io.File
import kotlinx.coroutines.delay
import android.app.Activity
import androidx.core.view.WindowCompat
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -128,6 +130,9 @@ fun ChatDetailScreen(
val coordinator = remember { KeyboardTransitionCoordinator() }
val view = LocalView.current
// 🎨 Window reference для управления статус баром
val window = remember { (view.context as? Activity)?.window }
// 🔥 Focus state for input
val inputFocusRequester = remember { FocusRequester() }
@@ -189,6 +194,24 @@ fun ChatDetailScreen(
var imageViewerInitialIndex by remember { mutableStateOf(0) }
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
// 🎨 Управление статус баром - чёрный при просмотре фото
DisposableEffect(isDarkTheme, showImageViewer) {
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
if (showImageViewer) {
// 📸 При просмотре фото - чёрный статус бар
window?.statusBarColor = 0xFF000000.toInt()
insetsController?.isAppearanceLightStatusBars = false
} else {
// Обычный режим - цвет хедера
val headerColor = if (isDarkTheme) 0xFF212121.toInt() else 0xFFFFFFFF.toInt()
window?.statusBarColor = headerColor
insetsController?.isAppearanceLightStatusBars = !isDarkTheme
}
onDispose { }
}
// 📷 Camera: URI для сохранения фото
var cameraImageUri by remember { mutableStateOf<Uri?>(null) }
@@ -532,6 +555,8 @@ fun ChatDetailScreen(
else Color.White
} else headerBackground
)
// 🎨 statusBarsPadding ПОСЛЕ background = хедер начинается под статус баром
.statusBarsPadding()
) {
// Контент хедера с Crossfade для плавной смены - ускоренная
// анимация
@@ -545,7 +570,6 @@ fun ChatDetailScreen(
Row(
modifier =
Modifier.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.padding(
horizontal =
@@ -723,7 +747,6 @@ fun ChatDetailScreen(
Row(
modifier =
Modifier.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.padding(
horizontal =

View File

@@ -65,6 +65,7 @@ import ja.burhanrashid52.photoeditor.PhotoEditorView
import ja.burhanrashid52.photoeditor.SaveSettings
import java.io.File
import kotlin.coroutines.resume
import androidx.core.view.WindowCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -162,15 +163,24 @@ fun ImageEditorScreen(
DisposableEffect(Unit) {
val originalStatusBarColor = window?.statusBarColor ?: 0
val originalNavigationBarColor = window?.navigationBarColor ?: 0
// Устанавливаем черный цвет
// 🔥 Сохраняем оригинальное состояние иконок статус бара
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
val originalLightStatusBars = insetsController?.isAppearanceLightStatusBars ?: false
val originalLightNavigationBars = insetsController?.isAppearanceLightNavigationBars ?: false
// Устанавливаем черный цвет и светлые иконки
window?.statusBarColor = android.graphics.Color.BLACK
window?.navigationBarColor = android.graphics.Color.BLACK
insetsController?.isAppearanceLightStatusBars = false // Светлые иконки на черном фоне
insetsController?.isAppearanceLightNavigationBars = false
onDispose {
// Восстанавливаем оригинальные цвета
// Восстанавливаем оригинальные цвета и состояние иконок
window?.statusBarColor = originalStatusBarColor
window?.navigationBarColor = originalNavigationBarColor
insetsController?.isAppearanceLightStatusBars = originalLightStatusBars
insetsController?.isAppearanceLightNavigationBars = originalLightNavigationBars
}
}
@@ -586,10 +596,10 @@ fun ImageEditorScreen(
Column(
modifier = Modifier
.fillMaxWidth()
// 🔥 imePadding СНАЧАЛА - поднимает над клавиатурой
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
// 🔥 Затем добавляем отступ для toolbar когда клавиатура закрыта
// 🔥 Отступ для toolbar ТОЛЬКО когда клавиатура/emoji закрыты
.padding(bottom = bottomPaddingForCaption)
// 🔥 imePadding поднимает над клавиатурой (применяется только когда emoji не показан)
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
) {
TelegramCaptionBar(
caption = caption,
@@ -1414,6 +1424,33 @@ fun MultiImageEditorScreen(
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val view = LocalView.current
// 🎨 Черный статус бар и навигационный бар для редактора
val window = remember { (view.context as? Activity)?.window }
DisposableEffect(Unit) {
val originalStatusBarColor = window?.statusBarColor ?: 0
val originalNavigationBarColor = window?.navigationBarColor ?: 0
// 🔥 Сохраняем оригинальное состояние иконок статус бара
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
val originalLightStatusBars = insetsController?.isAppearanceLightStatusBars ?: false
val originalLightNavigationBars = insetsController?.isAppearanceLightNavigationBars ?: false
// Устанавливаем черный цвет и светлые иконки
window?.statusBarColor = android.graphics.Color.BLACK
window?.navigationBarColor = android.graphics.Color.BLACK
insetsController?.isAppearanceLightStatusBars = false // Светлые иконки на черном фоне
insetsController?.isAppearanceLightNavigationBars = false
onDispose {
// Восстанавливаем оригинальные цвета и состояние иконок
window?.statusBarColor = originalStatusBarColor
window?.navigationBarColor = originalNavigationBarColor
insetsController?.isAppearanceLightStatusBars = originalLightStatusBars
insetsController?.isAppearanceLightNavigationBars = originalLightNavigationBars
}
}
val imagesWithCaptions = remember {
mutableStateListOf<ImageWithCaption>().apply {

View File

@@ -155,23 +155,22 @@ fun ImageViewerScreen(
}
}
// Close with animation
// Close with animation - красиво возвращаемся к пузырьку
fun closeWithAnimation() {
if (isClosing) return
isClosing = true
if (sourceBounds != null) {
scope.launch {
scope.launch {
if (sourceBounds != null) {
// Плавно возвращаемся к исходному положению пузырька
animationProgress.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 200,
easing = TelegramEasing
durationMillis = 280,
easing = FastOutSlowInEasing
)
)
onDismiss()
}
} else {
onDismiss()
}
}
@@ -192,17 +191,61 @@ fun ImageViewerScreen(
// UI visibility state
var showControls by remember { mutableStateOf(true) }
// Drag to dismiss
var offsetY by remember { mutableStateOf(0f) }
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// 🎬 ПРОСТОЙ FADE-OUT при свайпе
// ═══════════════════════════════════════════════════════════
var dragOffsetY by remember { mutableFloatStateOf(0f) }
var isDragging by remember { mutableStateOf(false) }
val dismissThreshold = 200f
var dragVelocity by remember { mutableFloatStateOf(0f) }
val dismissThreshold = 100f
val velocityThreshold = 500f
// Анимированные значения
val animatedOffsetY = remember { Animatable(0f) }
val dismissAlpha = remember { Animatable(1f) }
// Синхронизируем drag с анимацией
LaunchedEffect(isDragging, dragOffsetY) {
if (isDragging) {
animatedOffsetY.snapTo(dragOffsetY)
}
}
// Animated background alpha based on animation progress and drag
val baseAlpha = animationProgress.value
val dragAlpha = if (isDragging) {
(1f - (offsetY.absoluteValue / 500f)).coerceIn(0.3f, 1f)
} else 1f
val backgroundAlpha = baseAlpha * dragAlpha
// 🎬 Простой fade-out dismiss
fun smoothDismiss() {
if (isClosing) return
isClosing = true
scope.launch {
// Плавно исчезаем - более длинная анимация
dismissAlpha.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 350,
easing = LinearEasing
)
)
onDismiss()
}
}
// Плавный возврат на место
fun snapBack() {
scope.launch {
animatedOffsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
)
)
}
}
// Alpha на основе drag прогресса
val dismissProgress = (animatedOffsetY.value.absoluteValue / 300f).coerceIn(0f, 1f)
val backgroundAlpha = animationProgress.value * dismissAlpha.value * (1f - dismissProgress * 0.5f)
// Current image info
val currentImage = images.getOrNull(pagerState.currentPage)
@@ -212,37 +255,6 @@ fun ImageViewerScreen(
closeWithAnimation()
}
// ═══════════════════════════════════════════════════════════
// 🎬 CALCULATE ANIMATED TRANSFORMS (Telegram style)
// ═══════════════════════════════════════════════════════════
val progress = animationProgress.value
// Calculate interpolated values for shared element transition
val animatedScale: Float
val animatedTranslationX: Float
val animatedTranslationY: Float
val animatedCornerRadius: Float
if (sourceBounds != null && screenSize.width > 0 && screenSize.height > 0) {
// Source state (thumbnail in chat)
val sourceScale = sourceBounds.width / screenSize.width.toFloat()
val sourceCenterX = sourceBounds.left + sourceBounds.width / 2f
val sourceCenterY = sourceBounds.top + sourceBounds.height / 2f
val screenCenterX = screenSize.width / 2f
val screenCenterY = screenSize.height / 2f
// Interpolate between source and fullscreen
animatedScale = sourceScale + (1f - sourceScale) * progress
animatedTranslationX = (sourceCenterX - screenCenterX) * (1f - progress)
animatedTranslationY = (sourceCenterY - screenCenterY) * (1f - progress)
animatedCornerRadius = sourceBounds.cornerRadius * (1f - progress)
} else {
animatedScale = 1f
animatedTranslationX = 0f
animatedTranslationY = 0f
animatedCornerRadius = 0f
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -250,63 +262,86 @@ fun ImageViewerScreen(
.background(Color.Black.copy(alpha = backgroundAlpha))
) {
// ═══════════════════════════════════════════════════════════
// 📸 HORIZONTAL PAGER with shared element animation
// 📸 HORIZONTAL PAGER с Telegram-style эффектом наслаивания
// ═══════════════════════════════════════════════════════════
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
// Apply Telegram-style shared element transform
scaleX = animatedScale
scaleY = animatedScale
translationX = animatedTranslationX
translationY = animatedTranslationY + offsetY
// Clip with animated corner radius
clip = animatedCornerRadius > 0f
shape = if (animatedCornerRadius > 0f) {
RoundedCornerShape(animatedCornerRadius.dp)
} else {
RectangleShape
}
alpha = dismissAlpha.value
translationY = animatedOffsetY.value
},
pageSpacing = 30.dp, // Telegram: dp(30) между фото
key = { images[it].attachmentId }
) { page ->
val image = images[page]
// 🎬 Telegram-style наслаивание: правая страница уменьшается и затемняется
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
val isRightPage = pageOffset < 0
// Scale эффект только для правой страницы (1.0 → 0.7)
val scaleFactor = if (isRightPage) {
val progress = (-pageOffset).coerceIn(0f, 1f)
1f - (progress * 0.3f)
} else 1f
// Alpha для правой страницы
val pageAlpha = if (isRightPage) {
1f - ((-pageOffset).coerceIn(0f, 1f) * 0.3f)
} else 1f
ZoomableImage(
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX = scaleFactor
scaleY = scaleFactor
alpha = pageAlpha
}
) {
ZoomableImage(
image = image,
privateKey = privateKey,
onTap = { showControls = !showControls },
onVerticalDrag = { dragAmount ->
offsetY += dragAmount
onVerticalDrag = { dragAmount, velocity ->
dragOffsetY += dragAmount
dragVelocity = velocity
isDragging = true
},
onDragEnd = {
isDragging = false
if (offsetY.absoluteValue > dismissThreshold) {
closeWithAnimation()
val shouldDismiss = dragOffsetY.absoluteValue > dismissThreshold ||
dragVelocity.absoluteValue > velocityThreshold
if (shouldDismiss) {
smoothDismiss()
} else {
offsetY = 0f
dragOffsetY = 0f
dragVelocity = 0f
snapBack()
}
}
)
)
} // Закрытие Box wrapper
}
// ═══════════════════════════════════════════════════════════
// 🎛️ TOP BAR (показываем только когда анимация завершена)
// 🎛️ TOP BAR - Telegram-style (200ms, 24dp slide)
// ═══════════════════════════════════════════════════════════
AnimatedVisibility(
visible = showControls && animationState == 1 && !isClosing,
enter = fadeIn(tween(150)) + slideInVertically { -it },
exit = fadeOut(tween(100)) + slideOutVertically { -it },
enter = fadeIn(tween(200, easing = FastOutSlowInEasing)) +
slideInVertically(tween(200, easing = FastOutSlowInEasing)) { -96 }, // ~24dp
exit = fadeOut(tween(200, easing = FastOutSlowInEasing)) +
slideOutVertically(tween(200, easing = FastOutSlowInEasing)) { -96 },
modifier = Modifier.align(Alignment.TopCenter)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
Color.Black.copy(alpha = 0.6f)
Color.Black.copy(alpha = 0.5f)
)
.statusBarsPadding()
.padding(horizontal = 4.dp, vertical = 8.dp)
@@ -348,13 +383,15 @@ fun ImageViewerScreen(
}
// ═══════════════════════════════════════════════════════════
// 📍 PAGE INDICATOR (если больше 1 фото, показываем когда анимация завершена)
// 📍 PAGE INDICATOR - Telegram-style (200ms, 24dp slide снизу)
// ═══════════════════════════════════════════════════════════
if (images.size > 1) {
AnimatedVisibility(
visible = showControls && animationState == 1 && !isClosing,
enter = fadeIn(tween(150)),
exit = fadeOut(tween(100)),
enter = fadeIn(tween(200, easing = FastOutSlowInEasing)) +
slideInVertically(tween(200, easing = FastOutSlowInEasing)) { 96 }, // +24dp снизу
exit = fadeOut(tween(200, easing = FastOutSlowInEasing)) +
slideOutVertically(tween(200, easing = FastOutSlowInEasing)) { 96 },
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
@@ -406,14 +443,14 @@ fun ImageViewerScreen(
* - Double tap to zoom 2.5x / reset
* - Pinch to zoom 1x-5x
* - Pan when zoomed
* - Vertical drag to dismiss when not zoomed
* - Vertical drag to dismiss when not zoomed (with velocity tracking)
*/
@Composable
private fun ZoomableImage(
image: ViewableImage,
privateKey: String,
onTap: () -> Unit,
onVerticalDrag: (Float) -> Unit = {},
onVerticalDrag: (Float, Float) -> Unit = { _, _ -> }, // dragAmount, velocity
onDragEnd: () -> Unit = {}
) {
val context = LocalContext.current
@@ -564,84 +601,100 @@ private fun ZoomableImage(
}
.pointerInput(Unit) {
var isVerticalDragging = false
var lastDragTime = 0L
var lastDragY = 0f
var currentVelocity = 0f
val touchSlopValue = 20f // Минимальное смещение для определения направления
forEachGesture {
awaitPointerEventScope {
// Wait for first down
val down = awaitFirstDown(requireUnconsumed = false)
awaitEachGesture {
// Wait for first down
val down = awaitFirstDown(requireUnconsumed = false)
lastDragTime = System.currentTimeMillis()
lastDragY = down.position.y
currentVelocity = 0f
var zoom = 1f
var pastTouchSlop = false
var lockedToDismiss = false
do {
val event = awaitPointerEvent()
val canceled = event.changes.any { it.isConsumed }
var zoom = 1f
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToDismiss = false
do {
val event = awaitPointerEvent()
val canceled = event.changes.any { it.isConsumed }
if (!canceled) {
val zoomChange = event.calculateZoom()
val panChange = event.calculatePan()
if (!canceled) {
val zoomChange = event.calculateZoom()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val touchMoved = abs(panChange.x) > touchSlopValue || abs(panChange.y) > touchSlopValue
val zoomMotion = abs(1 - zoom) * centroidSize > touchSlopValue
if (!pastTouchSlop) {
zoom *= zoomChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val touchMoved = abs(panChange.x) > touchSlop || abs(panChange.y) > touchSlop
val zoomMotion = abs(1 - zoom) * centroidSize > touchSlop
if (touchMoved || zoomMotion) {
pastTouchSlop = true
if (touchMoved || zoomMotion) {
pastTouchSlop = true
// Decide: vertical dismiss or zoom/pan?
if (scale <= 1.05f && zoomChange == 1f &&
abs(panChange.y) > abs(panChange.x) * 1.5f) {
lockedToDismiss = true
isVerticalDragging = true
}
}
}
if (pastTouchSlop) {
if (lockedToDismiss) {
// Vertical drag for dismiss
onVerticalDrag(panChange.y)
event.changes.forEach { it.consume() }
} else {
// Zoom and pan
val newScale = (scale * zoomChange).coerceIn(minScale, maxScale)
// Calculate max offsets based on zoom
val maxX = (containerSize.width * (newScale - 1) / 2f).coerceAtLeast(0f)
val maxY = (containerSize.height * (newScale - 1) / 2f).coerceAtLeast(0f)
val newOffsetX = (offsetX + panChange.x).coerceIn(-maxX, maxX)
val newOffsetY = (offsetY + panChange.y).coerceIn(-maxY, maxY)
scale = newScale
offsetX = newOffsetX
offsetY = newOffsetY
// Consume if zoomed to prevent pager swipe
if (scale > 1.05f) {
event.changes.forEach { it.consume() }
}
// Decide: vertical dismiss or zoom/pan?
if (scale <= 1.05f && zoomChange == 1f &&
abs(panChange.y) > abs(panChange.x) * 1.5f) {
lockedToDismiss = true
isVerticalDragging = true
}
}
}
if (pastTouchSlop) {
if (lockedToDismiss) {
// Calculate velocity for smooth dismiss
val currentTime = System.currentTimeMillis()
val currentY = event.changes.firstOrNull()?.position?.y ?: lastDragY
val timeDelta = (currentTime - lastDragTime).coerceAtLeast(1L)
val positionDelta = currentY - lastDragY
// Velocity in px/second
currentVelocity = (positionDelta / timeDelta) * 1000f
lastDragTime = currentTime
lastDragY = currentY
// Vertical drag for dismiss with velocity
onVerticalDrag(panChange.y, currentVelocity)
event.changes.forEach { it.consume() }
} else {
// Zoom and pan
val newScale = (scale * zoomChange).coerceIn(minScale, maxScale)
// Calculate max offsets based on zoom
val maxX = (containerSize.width * (newScale - 1) / 2f).coerceAtLeast(0f)
val maxY = (containerSize.height * (newScale - 1) / 2f).coerceAtLeast(0f)
val newOffsetX = (offsetX + panChange.x).coerceIn(-maxX, maxX)
val newOffsetY = (offsetY + panChange.y).coerceIn(-maxY, maxY)
scale = newScale
offsetX = newOffsetX
offsetY = newOffsetY
// Consume if zoomed to prevent pager swipe
if (scale > 1.05f) {
event.changes.forEach { it.consume() }
}
}
}
} while (event.changes.any { it.pressed })
// Pointer up - end drag
if (isVerticalDragging) {
isVerticalDragging = false
onDragEnd()
}
// Snap back if scale is close to 1
if (scale < 1.05f) {
scale = 1f
offsetX = 0f
offsetY = 0f
}
} while (event.changes.any { it.pressed })
// Pointer up - end drag
if (isVerticalDragging) {
isVerticalDragging = false
onDragEnd()
}
// Snap back if scale is close to 1
if (scale < 1.05f) {
scale = 1f
offsetX = 0f
offsetY = 0f
}
}
},