add custom color for navigation rail

This commit is contained in:
2026-07-27 19:56:32 +03:00
parent cc8daa2a8f
commit 14a10e6d08
2 changed files with 79 additions and 47 deletions
@@ -5,6 +5,12 @@ import androiddev.apps.vbox.client.shared.generated.resources.not_found
import androiddev.apps.vbox.client.shared.generated.resources.recommendations
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -14,13 +20,12 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
@@ -34,21 +39,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.seiko.imageloader.rememberImagePainter
import org.jetbrains.compose.resources.StringResource
@@ -148,20 +150,18 @@ fun VideosInfo(
val requester = remember { FocusRequester() }
val blur = LocalBlurState.current
val gridState = rememberLazyGridState()
val layoutDirection = LocalLayoutDirection.current
Box(modifier = Modifier.fillMaxSize()) {
if (items.isNotEmpty()) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 125.dp),
columns = GridCells.Adaptive(minSize = 130.dp),
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
.applyBlurSource(blur),
contentPadding = PaddingValues(
start = paddingValues.calculateStartPadding(layoutDirection),
top = paddingValues.calculateTopPadding(),
end = paddingValues.calculateEndPadding(layoutDirection),
bottom = paddingValues.calculateBottomPadding()
),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = gridState
) {
itemsIndexed(
@@ -172,11 +172,8 @@ fun VideosInfo(
imageUrl = item.poster,
label = item.title,
onClick = { onClick(item) },
modifier = if (index == 0) {
Modifier.focusRequester(requester)
} else {
Modifier
}
modifier = Modifier
.then(if (index == 0) Modifier.focusRequester(requester) else Modifier)
)
}
}
@@ -184,9 +181,10 @@ fun VideosInfo(
VideosNotFound(emptyResource)
}
}
LaunchedEffect(items) {
// Мягкий фокус без withFrameNanos, реагирующий на появление данных
LaunchedEffect(items.isNotEmpty()) {
if (items.isNotEmpty()) {
withFrameNanos {}
requester.requestFocus()
}
}
@@ -199,54 +197,84 @@ fun Poster(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
var sizeImage by remember { mutableStateOf(IntSize.Zero) }
var focusState by remember { mutableStateOf<FocusState?>(null) }
val isFocused = focusState?.isFocused == true
var isFocused by remember { mutableStateOf(false) }
val gradient = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black),
startY = sizeImage.height.toFloat() / 3,
endY = sizeImage.height.toFloat()
// Анимируем увеличение карточки при фокусе (эффект для ТВ)
val scale by animateFloatAsState(
targetValue = if (isFocused) 1.08f else 1.0f,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "scale"
)
// Плавная анимация цвета обводки
val borderColor by animateColorAsState(
targetValue = if (isFocused) MaterialTheme.colorScheme.primary else Color.Transparent,
animationSpec = tween(200),
label = "borderColor"
)
// Плавная анимация толщины обводки
val borderWidth by animateDpAsState(
targetValue = if (isFocused) 3.dp else 0.dp,
animationSpec = tween(200),
label = "borderWidth"
)
Box(
modifier = modifier
.aspectRatio(0.8f)
.onFocusEvent { focusState = it }
.aspectRatio(0.7f) // Стандартное кинематографическое соотношение для постеров
.graphicsLayer {
// Применяем масштаб и скругление на уровне слоя (работает максимально быстро)
scaleX = scale
scaleY = scale
clip = true
shape = RoundedCornerShape(12.dp)
}
.onFocusChanged { isFocused = it.isFocused }
.border(
width = if (isFocused) 6.dp else 0.dp,
color = if (isFocused) {
MaterialTheme.colorScheme.secondaryContainer
} else {
Color.Transparent
}
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(12.dp)
)
.clickable(onClick = onClick)
) {
val painter = rememberImagePainter(imageUrl)
// Картинка постера
Image(
painter = painter,
painter = rememberImagePainter(imageUrl),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.onGloballyPositioned {
sizeImage = it.size
}.matchParentSize()
modifier = Modifier.fillMaxSize()
)
Box(modifier = Modifier.fillMaxSize().background(gradient))
// Оптимизированный затемняющий градиент (снизу вверх)
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
0.4f to Color.Transparent, // До 40% высоты прозрачно
1.0f to Color.Black.copy(alpha = 0.85f) // К низу плотное затемнение
)
)
)
// Текст названия фильма/сериала
Text(
text = label,
style = MaterialTheme.typography.bodySmall.copy(color = Color.White),
style = MaterialTheme.typography.bodyMedium.copy(
color = Color.White,
fontWeight = FontWeight.SemiBold
),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.align(Alignment.BottomStart)
.padding(8.dp)
.padding(12.dp)
)
}
}
@Composable
fun ErrorAndRetry(errorMsg: String, retry: () -> Unit) {
Column(
@@ -17,6 +17,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
@@ -160,7 +161,10 @@ private fun <T : Route> ExpandedContent(
val bottomPadding = values.calculateBottomPadding()
val endPadding = values.calculateEndPadding(LayoutDirection.Ltr)
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail(modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = topPadding)) {
NavigationRail(
modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = topPadding),
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) {
navigationEntries.forEach { entry ->
NavigationRailItem(
selected = currentRoute == entry.route,