feat: Implement navigation bar visibility handling based on navigation mode

This commit is contained in:
2026-02-09 10:34:40 +05:00
parent e1c119f621
commit 8dfcf1c410
7 changed files with 225 additions and 31 deletions

View File

@@ -228,11 +228,9 @@ fun ChatsListScreen(
insetsController.isAppearanceLightStatusBars = !isDarkTheme
window.statusBarColor = android.graphics.Color.TRANSPARENT
// Navigation bar — keep visible, match theme
insetsController.show(
androidx.core.view.WindowInsetsCompat.Type.navigationBars()
)
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
// Navigation bar: показываем только если есть нативные кнопки
com.rosetta.messenger.ui.utils.NavigationModeUtils
.applyNavigationBarVisibility(insetsController, context, isDarkTheme)
}
}

View File

@@ -104,8 +104,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
ChatsUiState()
)
// Загрузка
private val _isLoading = MutableStateFlow(false)
// Загрузка (🔥 true по умолчанию — skeleton на первом кадре, чтобы не мигало empty→skeleton→empty)
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val TAG = "ChatsListVM"
@@ -114,6 +114,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
fun setAccount(publicKey: String, privateKey: String) {
val setAccountStart = System.currentTimeMillis()
if (currentAccount == publicKey) {
// 🔥 Сбрасываем skeleton если он ещё показан (при повторном заходе)
if (_isLoading.value) _isLoading.value = false
return
}

View File

@@ -212,9 +212,17 @@ fun ImageEditorScreen(
onDispose {
if (window == null || insetsController == null) return@onDispose
window.statusBarColor = originalStatusBarColor
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
// 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
}
}
}
@@ -1573,9 +1581,17 @@ fun MultiImageEditorScreen(
onDispose {
if (window == null || insetsController == null) return@onDispose
window.statusBarColor = originalStatusBarColor
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
// 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
}
}
}

View File

@@ -139,9 +139,17 @@ fun InAppCameraScreen(
onDispose {
if (window == null || insetsController == null) return@onDispose
window.statusBarColor = originalStatusBarColor
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
// 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
}
}
}

View File

@@ -25,7 +25,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
@@ -43,9 +45,12 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.R
import com.rosetta.messenger.ui.theme.*
import com.rosetta.messenger.ui.utils.NavigationModeUtils
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
@@ -154,8 +159,12 @@ fun OnboardingScreen(
val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
window.statusBarColor = android.graphics.Color.TRANSPARENT
// Navigation bar: показываем только если есть нативные кнопки
NavigationModeUtils.applyNavigationBarVisibility(
insetsController, view.context, isDarkTheme
)
}
}
@@ -163,8 +172,16 @@ fun OnboardingScreen(
LaunchedEffect(Unit) {
if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window
window.navigationBarColor =
if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
val insetsController = WindowCompat.getInsetsController(window, view)
if (NavigationModeUtils.hasNativeNavigationBar(view.context)) {
window.navigationBarColor =
if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
} else {
// Жестовая навигация — прячем бар
insetsController.hide(WindowInsetsCompat.Type.navigationBars())
insetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@@ -685,7 +702,7 @@ fun GooeyPagerIndicator(
modifier: Modifier = Modifier,
dotRadius: Dp = 2.8.dp,
dotSpacing: Dp = 12.dp,
indicatorHeight: Dp = 18.dp
indicatorHeight: Dp = 18.dp,
) {
if (pageCount <= 0) return
@@ -699,6 +716,17 @@ fun GooeyPagerIndicator(
val trackWidth = if (pageCount > 1) spacing * (pageCount - 1) else 0f
val startX = (size.width - trackWidth) / 2f
if (pageCount > 1) {
val trackInset = baseRadius * 0.75f
val trackHeight = baseRadius * 0.45f
drawRoundRect(
color = unselectedColor.copy(alpha = 0.14f),
topLeft = Offset(startX - trackInset, centerY - trackHeight / 2f),
size = Size(trackWidth + trackInset * 2f, trackHeight),
cornerRadius = CornerRadius(trackHeight, trackHeight)
)
}
val rawPosition =
(pagerState.currentPage + pagerState.currentPageOffsetFraction)
.coerceIn(0f, (pageCount - 1).toFloat())
@@ -706,35 +734,77 @@ fun GooeyPagerIndicator(
repeat(pageCount) { index ->
val center = Offset(startX + index * spacing, centerY)
drawCircle(color = unselectedColor, radius = baseRadius, center = center)
val distanceToActive = abs(rawPosition - index)
val influence = (1f - distanceToActive / 1.25f).coerceIn(0f, 1f)
val dotScale = 0.88f + influence * 0.16f
val dotAlpha = 0.35f + influence * 0.35f
drawCircle(
color = unselectedColor.copy(alpha = unselectedColor.alpha * dotAlpha),
radius = baseRadius * dotScale,
center = center
)
}
val from = floor(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1)
val to = ceil(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1)
val transition = rawPosition - from
val stretch = (1f - abs(transition - 0.5f) * 2f).coerceIn(0f, 1f)
val activeRadius = baseRadius * (1.08f + stretch * 0.34f)
val activeRadius = baseRadius * (1.05f + stretch * 0.3f)
var metaballPath: Path? = null
var anchorCenter: Offset? = null
var anchorRadius = 0f
if (from != to) {
val anchorIndex = if (transition < 0.5f) from else to
val anchorCenter = Offset(startX + anchorIndex * spacing, centerY)
val anchorRadius = baseRadius * (1.0f - stretch * 0.1f)
anchorCenter = Offset(startX + anchorIndex * spacing, centerY)
anchorRadius = baseRadius * (0.86f + (1f - stretch) * 0.2f)
createMetaballPath(
metaballPath =
createMetaballPath(
c1 = activeCenter,
r1 = activeRadius,
c2 = anchorCenter,
r2 = anchorRadius,
maxDistance = spacing * 1.28f,
viscosity = 0.32f,
handleSize = 2.25f
maxDistance = spacing * 1.36f,
viscosity = 0.36f + stretch * 0.12f,
handleSize = 2.45f
)
?.let { path ->
drawPath(path = path, color = selectedColor.copy(alpha = 0.92f))
}
}
drawCircle(
color = selectedColor.copy(alpha = 0.18f),
radius = activeRadius * 2.2f,
center = activeCenter
)
if (anchorCenter != null) {
drawCircle(
color = selectedColor.copy(alpha = 0.1f + stretch * 0.1f),
radius = anchorRadius * 1.9f,
center = anchorCenter
)
}
metaballPath?.let { path ->
drawPath(path = path, color = selectedColor.copy(alpha = 0.2f + stretch * 0.12f))
}
if (anchorCenter != null) {
drawCircle(
color = selectedColor.copy(alpha = 0.78f),
radius = anchorRadius,
center = anchorCenter
)
}
metaballPath?.let { path -> drawPath(path = path, color = selectedColor.copy(alpha = 0.92f)) }
drawCircle(color = selectedColor, radius = activeRadius, center = activeCenter)
drawCircle(
color = Color.White.copy(alpha = 0.28f),
radius = activeRadius * 0.38f,
center =
Offset(
x = activeCenter.x - activeRadius * 0.28f,
y = activeCenter.y - activeRadius * 0.32f
)
)
}
}

View File

@@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import com.rosetta.messenger.ui.utils.NavigationModeUtils
import kotlinx.coroutines.delay
private val DarkColorScheme = darkColorScheme(
@@ -63,14 +64,17 @@ fun RosettaAndroidTheme(
else -> LightColorScheme
}
val view = LocalView.current
val context = LocalContext.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view)
// Make status bar transparent for wave animation overlay
window.statusBarColor = AndroidColor.TRANSPARENT
// Navigation bar color is managed by OnboardingScreen for smooth transition
// Don't change it here to avoid instant color change
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
insetsController.isAppearanceLightStatusBars = !darkTheme
// Navigation bar: показываем только если есть нативные кнопки
NavigationModeUtils.applyNavigationBarVisibility(insetsController, context, darkTheme)
}
}

View File

@@ -0,0 +1,96 @@
package com.rosetta.messenger.ui.utils
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
/**
* Утилита для определения типа системной навигации.
*
* Android поддерживает 3 режима:
* - 0 = 3-button navigation (нативные кнопки: Back, Home, Recents)
* - 1 = 2-button navigation (кнопка Home + жест назад)
* - 2 = Gesture navigation (полностью жестовая навигация, без кнопок)
*
* Если у устройства gesture navigation (2), нижний navigation bar прячем.
* Если у устройства кнопочная навигация (0 или 1), показываем navigation bar.
*/
object NavigationModeUtils {
private const val NAV_MODE_THREE_BUTTON = 0
private const val NAV_MODE_TWO_BUTTON = 1
private const val NAV_MODE_GESTURE = 2
/**
* Возвращает текущий режим навигации.
* 0 = 3-button, 1 = 2-button, 2 = gesture
*/
fun getNavigationMode(context: Context): Int {
return try {
val resId = context.resources.getIdentifier(
"config_navBarInteractionMode", "integer", "android"
)
if (resId > 0) context.resources.getInteger(resId) else NAV_MODE_THREE_BUTTON
} catch (_: Exception) {
NAV_MODE_THREE_BUTTON
}
}
/**
* true если устройство использует жестовую навигацию (без нативных кнопок внизу)
*/
fun isGestureNavigation(context: Context): Boolean {
return getNavigationMode(context) == NAV_MODE_GESTURE
}
/**
* true если у устройства есть нативная панель навигации (3 или 2 кнопки)
*/
fun hasNativeNavigationBar(context: Context): Boolean {
return !isGestureNavigation(context)
}
/**
* Показывает или прячет navigation bar в зависимости от типа навигации.
* - Кнопочная навигация → показываем бар
* - Жестовая навигация → прячем бар, свайп снизу временно покажет
*/
fun applyNavigationBarVisibility(
insetsController: WindowInsetsControllerCompat,
context: Context,
isDarkTheme: Boolean
) {
if (hasNativeNavigationBar(context)) {
// Есть нативные кнопки — показываем навигационный бар
insetsController.show(WindowInsetsCompat.Type.navigationBars())
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
} else {
// Жестовая навигация — прячем навигационный бар
insetsController.hide(WindowInsetsCompat.Type.navigationBars())
insetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
/**
* Composable-хелпер: запоминает, использует ли устройство жестовую навигацию.
*/
@Composable
fun rememberIsGestureNavigation(): Boolean {
val context = LocalContext.current
return remember { NavigationModeUtils.isGestureNavigation(context) }
}
/**
* Composable-хелпер: запоминает, есть ли нативная навигационная панель.
*/
@Composable
fun rememberHasNativeNavigationBar(): Boolean {
val context = LocalContext.current
return remember { NavigationModeUtils.hasNativeNavigationBar(context) }
}