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

View File

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

View File

@@ -212,9 +212,17 @@ fun ImageEditorScreen(
onDispose { onDispose {
if (window == null || insetsController == null) return@onDispose if (window == null || insetsController == null) return@onDispose
window.statusBarColor = originalStatusBarColor window.statusBarColor = originalStatusBarColor
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightStatusBars = originalLightStatusBars insetsController.isAppearanceLightStatusBars = originalLightStatusBars
// Navigation bar: восстанавливаем только если есть нативные кнопки
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars 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 { onDispose {
if (window == null || insetsController == null) return@onDispose if (window == null || insetsController == null) return@onDispose
window.statusBarColor = originalStatusBarColor window.statusBarColor = originalStatusBarColor
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightStatusBars = originalLightStatusBars insetsController.isAppearanceLightStatusBars = originalLightStatusBars
// Navigation bar: восстанавливаем только если есть нативные кнопки
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars 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 { onDispose {
if (window == null || insetsController == null) return@onDispose if (window == null || insetsController == null) return@onDispose
window.statusBarColor = originalStatusBarColor window.statusBarColor = originalStatusBarColor
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightStatusBars = originalLightStatusBars insetsController.isAppearanceLightStatusBars = originalLightStatusBars
// Navigation bar: восстанавливаем только если есть нативные кнопки
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
window.navigationBarColor = originalNavigationBarColor
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars 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.drawWithContent
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.airbnb.lottie.compose.* import com.airbnb.lottie.compose.*
import com.rosetta.messenger.R import com.rosetta.messenger.R
import com.rosetta.messenger.ui.theme.* import com.rosetta.messenger.ui.theme.*
import com.rosetta.messenger.ui.utils.NavigationModeUtils
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.acos import kotlin.math.acos
@@ -154,8 +159,12 @@ fun OnboardingScreen(
val window = (view.context as android.app.Activity).window val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view) val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !isDarkTheme insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
window.statusBarColor = android.graphics.Color.TRANSPARENT window.statusBarColor = android.graphics.Color.TRANSPARENT
// Navigation bar: показываем только если есть нативные кнопки
NavigationModeUtils.applyNavigationBarVisibility(
insetsController, view.context, isDarkTheme
)
} }
} }
@@ -163,8 +172,16 @@ fun OnboardingScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (!view.isInEditMode) { if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view)
if (NavigationModeUtils.hasNativeNavigationBar(view.context)) {
window.navigationBarColor = window.navigationBarColor =
if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() 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, modifier: Modifier = Modifier,
dotRadius: Dp = 2.8.dp, dotRadius: Dp = 2.8.dp,
dotSpacing: Dp = 12.dp, dotSpacing: Dp = 12.dp,
indicatorHeight: Dp = 18.dp indicatorHeight: Dp = 18.dp,
) { ) {
if (pageCount <= 0) return if (pageCount <= 0) return
@@ -699,6 +716,17 @@ fun GooeyPagerIndicator(
val trackWidth = if (pageCount > 1) spacing * (pageCount - 1) else 0f val trackWidth = if (pageCount > 1) spacing * (pageCount - 1) else 0f
val startX = (size.width - trackWidth) / 2f 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 = val rawPosition =
(pagerState.currentPage + pagerState.currentPageOffsetFraction) (pagerState.currentPage + pagerState.currentPageOffsetFraction)
.coerceIn(0f, (pageCount - 1).toFloat()) .coerceIn(0f, (pageCount - 1).toFloat())
@@ -706,35 +734,77 @@ fun GooeyPagerIndicator(
repeat(pageCount) { index -> repeat(pageCount) { index ->
val center = Offset(startX + index * spacing, centerY) 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 from = floor(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1)
val to = ceil(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1) val to = ceil(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1)
val transition = rawPosition - from val transition = rawPosition - from
val stretch = (1f - abs(transition - 0.5f) * 2f).coerceIn(0f, 1f) 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) { if (from != to) {
val anchorIndex = if (transition < 0.5f) from else to val anchorIndex = if (transition < 0.5f) from else to
val anchorCenter = Offset(startX + anchorIndex * spacing, centerY) anchorCenter = Offset(startX + anchorIndex * spacing, centerY)
val anchorRadius = baseRadius * (1.0f - stretch * 0.1f) anchorRadius = baseRadius * (0.86f + (1f - stretch) * 0.2f)
metaballPath =
createMetaballPath( createMetaballPath(
c1 = activeCenter, c1 = activeCenter,
r1 = activeRadius, r1 = activeRadius,
c2 = anchorCenter, c2 = anchorCenter,
r2 = anchorRadius, r2 = anchorRadius,
maxDistance = spacing * 1.28f, maxDistance = spacing * 1.36f,
viscosity = 0.32f, viscosity = 0.36f + stretch * 0.12f,
handleSize = 2.25f 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 = 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.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.rosetta.messenger.ui.utils.NavigationModeUtils
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
@@ -63,14 +64,17 @@ fun RosettaAndroidTheme(
else -> LightColorScheme else -> LightColorScheme
} }
val view = LocalView.current val view = LocalView.current
val context = LocalContext.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as android.app.Activity).window val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view)
// Make status bar transparent for wave animation overlay // Make status bar transparent for wave animation overlay
window.statusBarColor = AndroidColor.TRANSPARENT window.statusBarColor = AndroidColor.TRANSPARENT
// Navigation bar color is managed by OnboardingScreen for smooth transition insetsController.isAppearanceLightStatusBars = !darkTheme
// Don't change it here to avoid instant color change
WindowCompat.getInsetsController(window, view).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) }
}