From 14a10e6d085403fcf64a2f82c82e68ab6c0f848d Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 27 Jul 2026 19:56:32 +0300 Subject: [PATCH] add custom color for navigation rail --- .../shared/presentation/list/VideoScreen.kt | 120 +++++++++++------- .../kotlin/ru/shadowsparky/ui/BaseScreen.kt | 6 +- 2 files changed, 79 insertions(+), 47 deletions(-) diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt index 665bf15..6ff1d7b 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt @@ -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(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( diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt index 5d60726..cdcd2b5 100644 --- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt +++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt @@ -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 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,