feat: Implement navigation bar visibility handling based on navigation mode
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
Reference in New Issue
Block a user