feat: implement system bars style utility for consistent UI behavior
This commit is contained in:
@@ -52,7 +52,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -79,6 +78,7 @@ import com.rosetta.messenger.ui.chats.utils.*
|
||||
import com.rosetta.messenger.ui.components.AvatarImage
|
||||
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.utils.SystemBarsStyleUtils
|
||||
import com.rosetta.messenger.utils.MediaUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@@ -191,22 +191,17 @@ fun ChatDetailScreen(
|
||||
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
||||
|
||||
// 🎨 Управление статус баром
|
||||
DisposableEffect(isDarkTheme, showImageViewer) {
|
||||
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
|
||||
|
||||
DisposableEffect(isDarkTheme, showImageViewer, window, view) {
|
||||
if (showImageViewer) {
|
||||
// 📸 При просмотре фото - чёрный статус бар
|
||||
window?.statusBarColor = android.graphics.Color.BLACK
|
||||
insetsController?.isAppearanceLightStatusBars = false
|
||||
// 📸 При просмотре фото - чёрные system bars
|
||||
SystemBarsStyleUtils.applyFullscreenDark(window, view)
|
||||
} else {
|
||||
// Обычный режим - прозрачный статус бар, иконки по теме
|
||||
window?.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
insetsController?.isAppearanceLightStatusBars = !isDarkTheme
|
||||
// Обычный режим чата
|
||||
SystemBarsStyleUtils.applyChatStatusBar(window, view, isDarkTheme)
|
||||
}
|
||||
|
||||
onDispose {
|
||||
// Восстанавливаем прозрачный статус бар при выходе
|
||||
window?.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
SystemBarsStyleUtils.applyChatStatusBar(window, view, isDarkTheme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1990,11 +1985,7 @@ fun ChatDetailScreen(
|
||||
},
|
||||
onClosingStart = {
|
||||
// Сразу сбрасываем status bar при начале закрытия (до анимации)
|
||||
window?.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
window?.let { w ->
|
||||
WindowCompat.getInsetsController(w, view)
|
||||
?.isAppearanceLightStatusBars = !isDarkTheme
|
||||
}
|
||||
SystemBarsStyleUtils.applyChatStatusBar(window, view, isDarkTheme)
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
sourceBounds = imageViewerSourceBounds
|
||||
|
||||
@@ -60,6 +60,7 @@ import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
||||
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.utils.SystemBarsStyleUtils
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
@@ -68,7 +69,6 @@ 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
|
||||
@@ -170,59 +170,26 @@ fun ImageEditorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 Плавная анимация status bar синхронно с fade
|
||||
// 🎨 System bars style
|
||||
val activity = context as? Activity
|
||||
val window = activity?.window
|
||||
|
||||
// Сохраняем оригинальные цвета один раз
|
||||
val originalStatusBarColor = remember { window?.statusBarColor ?: android.graphics.Color.WHITE }
|
||||
val originalNavigationBarColor = remember { window?.navigationBarColor ?: android.graphics.Color.WHITE }
|
||||
val insetsController = remember(window, view) { window?.let { WindowCompat.getInsetsController(it, view) } }
|
||||
val originalLightStatusBars = remember { insetsController?.isAppearanceLightStatusBars ?: true }
|
||||
val originalLightNavigationBars = remember { insetsController?.isAppearanceLightNavigationBars ?: true }
|
||||
|
||||
// ⚡ Анимация цвета status bar синхронно с fade
|
||||
LaunchedEffect(animationProgress.value) {
|
||||
if (window == null || insetsController == null) return@LaunchedEffect
|
||||
|
||||
val progress = animationProgress.value
|
||||
|
||||
// Интерполируем цвет: оригинальный (progress=0) -> черный (progress=1)
|
||||
val currentStatusColor = androidx.core.graphics.ColorUtils.blendARGB(
|
||||
originalStatusBarColor,
|
||||
android.graphics.Color.BLACK,
|
||||
progress
|
||||
)
|
||||
val currentNavColor = androidx.core.graphics.ColorUtils.blendARGB(
|
||||
originalNavigationBarColor,
|
||||
android.graphics.Color.BLACK,
|
||||
progress
|
||||
)
|
||||
|
||||
window.statusBarColor = currentStatusColor
|
||||
window.navigationBarColor = currentNavColor
|
||||
|
||||
// Иконки: светлые (false) когда progress > 0.5, иначе оригинальные
|
||||
insetsController.isAppearanceLightStatusBars = progress < 0.5f && originalLightStatusBars
|
||||
insetsController.isAppearanceLightNavigationBars = progress < 0.5f && originalLightNavigationBars
|
||||
val systemBarsState = remember(window, view) {
|
||||
SystemBarsStyleUtils.capture(window, view)
|
||||
}
|
||||
|
||||
// Восстановление при dispose
|
||||
DisposableEffect(window) {
|
||||
LaunchedEffect(window, view) {
|
||||
SystemBarsStyleUtils.applyFullscreenDark(window, view)
|
||||
}
|
||||
|
||||
DisposableEffect(window, view, isDarkTheme, systemBarsState) {
|
||||
onDispose {
|
||||
if (window == null || insetsController == null) return@onDispose
|
||||
window.statusBarColor = originalStatusBarColor
|
||||
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
|
||||
|
||||
// Navigation bar: восстанавливаем только если есть нативные кнопки
|
||||
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
|
||||
window.navigationBarColor = originalNavigationBarColor
|
||||
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
|
||||
} else {
|
||||
insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars())
|
||||
insetsController.systemBarsBehavior =
|
||||
androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
SystemBarsStyleUtils.restoreChatAfterFullscreen(
|
||||
window = window,
|
||||
view = view,
|
||||
context = context,
|
||||
isDarkTheme = isDarkTheme,
|
||||
state = systemBarsState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,8 +375,12 @@ fun ImageEditorScreen(
|
||||
down.consume() // Поглощаем все touch события
|
||||
}
|
||||
}
|
||||
.graphicsLayer { alpha = animationProgress.value } // ⚡ Всё плавно fade
|
||||
.background(Color.Black)
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.graphicsLayer { alpha = animationProgress.value }
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📸 FULLSCREEN PHOTO - занимает ВЕСЬ экран, не реагирует на клавиатуру
|
||||
@@ -779,6 +750,7 @@ fun ImageEditorScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1539,59 +1511,26 @@ fun MultiImageEditorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 Плавная анимация status bar синхронно с fade
|
||||
// 🎨 System bars style
|
||||
val activity = context as? Activity
|
||||
val window = activity?.window
|
||||
|
||||
// Сохраняем оригинальные цвета один раз
|
||||
val originalStatusBarColor = remember { window?.statusBarColor ?: android.graphics.Color.WHITE }
|
||||
val originalNavigationBarColor = remember { window?.navigationBarColor ?: android.graphics.Color.WHITE }
|
||||
val insetsController = remember(window, view) { window?.let { WindowCompat.getInsetsController(it, view) } }
|
||||
val originalLightStatusBars = remember { insetsController?.isAppearanceLightStatusBars ?: true }
|
||||
val originalLightNavigationBars = remember { insetsController?.isAppearanceLightNavigationBars ?: true }
|
||||
|
||||
// ⚡ Анимация цвета status bar синхронно с fade
|
||||
LaunchedEffect(animationProgress.value) {
|
||||
if (window == null || insetsController == null) return@LaunchedEffect
|
||||
|
||||
val progress = animationProgress.value
|
||||
|
||||
// Интерполируем цвет: оригинальный (progress=0) -> черный (progress=1)
|
||||
val currentStatusColor = androidx.core.graphics.ColorUtils.blendARGB(
|
||||
originalStatusBarColor,
|
||||
android.graphics.Color.BLACK,
|
||||
progress
|
||||
)
|
||||
val currentNavColor = androidx.core.graphics.ColorUtils.blendARGB(
|
||||
originalNavigationBarColor,
|
||||
android.graphics.Color.BLACK,
|
||||
progress
|
||||
)
|
||||
|
||||
window.statusBarColor = currentStatusColor
|
||||
window.navigationBarColor = currentNavColor
|
||||
|
||||
// Иконки: светлые (false) когда progress > 0.5, иначе оригинальные
|
||||
insetsController.isAppearanceLightStatusBars = progress < 0.5f && originalLightStatusBars
|
||||
insetsController.isAppearanceLightNavigationBars = progress < 0.5f && originalLightNavigationBars
|
||||
val systemBarsState = remember(window, view) {
|
||||
SystemBarsStyleUtils.capture(window, view)
|
||||
}
|
||||
|
||||
// Восстановление при dispose
|
||||
DisposableEffect(window) {
|
||||
LaunchedEffect(window, view) {
|
||||
SystemBarsStyleUtils.applyFullscreenDark(window, view)
|
||||
}
|
||||
|
||||
DisposableEffect(window, view, isDarkTheme, systemBarsState) {
|
||||
onDispose {
|
||||
if (window == null || insetsController == null) return@onDispose
|
||||
window.statusBarColor = originalStatusBarColor
|
||||
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
|
||||
|
||||
// Navigation bar: восстанавливаем только если есть нативные кнопки
|
||||
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
|
||||
window.navigationBarColor = originalNavigationBarColor
|
||||
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
|
||||
} else {
|
||||
insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars())
|
||||
insetsController.systemBarsBehavior =
|
||||
androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
SystemBarsStyleUtils.restoreChatAfterFullscreen(
|
||||
window = window,
|
||||
view = view,
|
||||
context = context,
|
||||
isDarkTheme = isDarkTheme,
|
||||
state = systemBarsState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1648,8 +1587,12 @@ fun MultiImageEditorScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = animationProgress.value } // ⚡ Всё плавно fade
|
||||
.background(Color.Black)
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.graphicsLayer { alpha = animationProgress.value }
|
||||
) {
|
||||
// Pager
|
||||
HorizontalPager(
|
||||
@@ -1672,7 +1615,8 @@ fun MultiImageEditorScreen(
|
||||
// Загружаем изображение
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val inputStream = ctx.contentResolver.openInputStream(imagesWithCaptions[page].uri)
|
||||
val inputStream =
|
||||
ctx.contentResolver.openInputStream(imagesWithCaptions[page].uri)
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
|
||||
@@ -1701,7 +1645,8 @@ fun MultiImageEditorScreen(
|
||||
if (currentUri != null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(currentUri)
|
||||
val inputStream =
|
||||
context.contentResolver.openInputStream(currentUri)
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
|
||||
@@ -1959,6 +1904,7 @@ fun MultiImageEditorScreen(
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Async image loader */
|
||||
|
||||
@@ -618,28 +618,28 @@ fun MediaPickerBottomSheet(
|
||||
animatedClose()
|
||||
onOpenCamera()
|
||||
},
|
||||
onItemClick = { item, position ->
|
||||
// Telegram-style: клик на фото сразу открывает редактор
|
||||
if (!item.isVideo) {
|
||||
thumbnailPosition = position
|
||||
// Сразу открываем редактор - галерея закроется автоматически
|
||||
editingItem = item
|
||||
} else {
|
||||
// Для видео - добавляем/убираем из selection
|
||||
onItemClick = { item, _ ->
|
||||
// Telegram-style selection:
|
||||
// Tap toggles selection for both photos and videos.
|
||||
if (item.id in selectedItems) {
|
||||
selectedItems = selectedItems - item.id
|
||||
} else if (selectedItems.size < maxSelection) {
|
||||
selectedItems = selectedItems + item.id
|
||||
}
|
||||
}
|
||||
},
|
||||
onItemLongClick = { item ->
|
||||
// Long press - снять выделение если выбрана
|
||||
// Long press keeps quick edit for photos.
|
||||
if (!item.isVideo) {
|
||||
thumbnailPosition = null
|
||||
editingItem = item
|
||||
} else {
|
||||
// Videos: keep long-press toggle behavior.
|
||||
if (item.id in selectedItems) {
|
||||
selectedItems = selectedItems - item.id
|
||||
} else if (selectedItems.size < maxSelection) {
|
||||
selectedItems = selectedItems + item.id
|
||||
}
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.weight(1f)
|
||||
|
||||
@@ -1372,17 +1372,20 @@ private fun TelegramTextField(
|
||||
targetValue = if (hasError) errorColor else Color.Transparent,
|
||||
label = "profile_field_border_color"
|
||||
)
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier =
|
||||
val fieldModifier =
|
||||
if (hasError) {
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(containerColor)
|
||||
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
} else {
|
||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
}
|
||||
|
||||
Column {
|
||||
Column(modifier = fieldModifier) {
|
||||
if (isEditable && onValueChange != null) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.rosetta.messenger.ui.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
|
||||
data class SystemBarsState(
|
||||
val statusBarColor: Int,
|
||||
val navigationBarColor: Int,
|
||||
val isLightStatusBars: Boolean,
|
||||
val isLightNavigationBars: Boolean
|
||||
)
|
||||
|
||||
object SystemBarsStyleUtils {
|
||||
|
||||
fun capture(window: Window?, view: View?): SystemBarsState? {
|
||||
if (window == null || view == null) return null
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
return SystemBarsState(
|
||||
statusBarColor = window.statusBarColor,
|
||||
navigationBarColor = window.navigationBarColor,
|
||||
isLightStatusBars = insetsController.isAppearanceLightStatusBars,
|
||||
isLightNavigationBars = insetsController.isAppearanceLightNavigationBars
|
||||
)
|
||||
}
|
||||
|
||||
fun applyFullscreenDark(window: Window?, view: View?) {
|
||||
if (window == null || view == null) return
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
window.statusBarColor = Color.BLACK
|
||||
window.navigationBarColor = Color.BLACK
|
||||
insetsController.isAppearanceLightStatusBars = false
|
||||
insetsController.isAppearanceLightNavigationBars = false
|
||||
}
|
||||
|
||||
fun applyChatStatusBar(window: Window?, view: View?, isDarkTheme: Boolean) {
|
||||
if (window == null || view == null) return
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||
}
|
||||
|
||||
fun restoreNavigationBar(window: Window?, view: View?, context: Context, state: SystemBarsState?) {
|
||||
if (window == null || view == null) return
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
|
||||
if (NavigationModeUtils.hasNativeNavigationBar(context)) {
|
||||
insetsController.show(WindowInsetsCompat.Type.navigationBars())
|
||||
if (state != null) {
|
||||
window.navigationBarColor = state.navigationBarColor
|
||||
insetsController.isAppearanceLightNavigationBars = state.isLightNavigationBars
|
||||
}
|
||||
} else {
|
||||
insetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
insetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreChatAfterFullscreen(
|
||||
window: Window?,
|
||||
view: View?,
|
||||
context: Context,
|
||||
isDarkTheme: Boolean,
|
||||
state: SystemBarsState?
|
||||
) {
|
||||
applyChatStatusBar(window, view, isDarkTheme)
|
||||
restoreNavigationBar(window, view, context, state)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user