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