- цветовые токены тёмно-зелёной темы + RadiolaColors (CompositionLocal) - darkColorScheme + всегда тёмная тема, фирменные shapes - типографика с весами/размерами под макет - Brand: AppMark (градиентный R), RadiolaWordmark, MonoMark - Motion: спеки движения, pressScale, живой эквалайзер - pill-таб-бар с анимированной активной вкладкой
131 lines
4.8 KiB
Kotlin
131 lines
4.8 KiB
Kotlin
package com.radiola.ui.navigation
|
|
|
|
import androidx.compose.animation.AnimatedVisibility
|
|
import androidx.compose.animation.animateColorAsState
|
|
import androidx.compose.animation.core.tween
|
|
import androidx.compose.animation.expandHorizontally
|
|
import androidx.compose.animation.fadeIn
|
|
import androidx.compose.animation.fadeOut
|
|
import androidx.compose.animation.shrinkHorizontally
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.border
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.navigation.NavController
|
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
import com.radiola.ui.theme.Motion
|
|
import com.radiola.ui.theme.RadiolaTheme
|
|
|
|
@Composable
|
|
fun BottomNavBar(navController: NavController) {
|
|
val colors = RadiolaTheme.colors
|
|
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
|
val items = NavDestinations.items.filter { it.showInBottomBar }
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp)
|
|
.clip(RoundedCornerShape(36.dp))
|
|
.background(colors.surface2)
|
|
.border(1.dp, colors.border, RoundedCornerShape(36.dp))
|
|
.height(62.dp)
|
|
.padding(6.dp),
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
items.forEach { destination ->
|
|
val selected = currentRoute == destination.route
|
|
PillTab(
|
|
label = destination.labelRes,
|
|
icon = destination.icon,
|
|
selected = selected,
|
|
modifier = Modifier.weight(if (selected) 1.9f else 1f),
|
|
onClick = {
|
|
if (currentRoute != destination.route) {
|
|
navController.navigate(destination.route) {
|
|
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
|
launchSingleTop = true
|
|
restoreState = true
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun PillTab(
|
|
label: String,
|
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
|
selected: Boolean,
|
|
onClick: () -> Unit,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
val colors = RadiolaTheme.colors
|
|
val bg by animateColorAsState(
|
|
targetValue = if (selected) colors.accent else androidx.compose.ui.graphics.Color.Transparent,
|
|
animationSpec = tween(Motion.Medium),
|
|
label = "tabBg"
|
|
)
|
|
val content by animateColorAsState(
|
|
targetValue = if (selected) colors.bgBase else colors.textSecondary,
|
|
animationSpec = tween(Motion.Medium),
|
|
label = "tabFg"
|
|
)
|
|
Row(
|
|
modifier = modifier
|
|
.fillMaxWidth()
|
|
.height(50.dp)
|
|
.clip(RoundedCornerShape(26.dp))
|
|
.background(bg)
|
|
.clickable(
|
|
interactionSource = remember { MutableInteractionSource() },
|
|
indication = null,
|
|
onClick = onClick
|
|
),
|
|
horizontalArrangement = Arrangement.Center,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
imageVector = icon,
|
|
contentDescription = label,
|
|
tint = content,
|
|
modifier = Modifier.height(18.dp).width(18.dp)
|
|
)
|
|
AnimatedVisibility(
|
|
visible = selected,
|
|
enter = fadeIn(tween(Motion.Medium)) + expandHorizontally(tween(Motion.Medium)),
|
|
exit = fadeOut(tween(Motion.Fast)) + shrinkHorizontally(tween(Motion.Fast))
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Spacer(Modifier.width(8.dp))
|
|
Text(
|
|
text = label.uppercase(),
|
|
color = content,
|
|
style = androidx.compose.material3.MaterialTheme.typography.labelSmall,
|
|
maxLines = 1
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|