From 32667ca9f315ea729daa7021607289d529e45378 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 27 Jul 2026 21:01:19 +0300 Subject: [PATCH] update expressive lazy column --- .../presentation/overview/OverviewScreen.kt | 52 +++++++++++---- .../shadowsparky/ui/components/Expressive.kt | 66 +++++++++---------- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt index 5e6d77d..e83304d 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt @@ -14,6 +14,12 @@ import androiddev.apps.vbox.client.shared.generated.resources.star_unchecked import androiddev.apps.vbox.client.shared.generated.resources.user_tags import androiddev.apps.vbox.client.shared.generated.resources.watch import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -44,7 +50,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -251,6 +256,7 @@ fun DetailsInfo( val requester = remember { FocusRequester() } val blur = LocalBlurState.current val showDetails = rememberSaveable { mutableStateOf(false) } + LazyColumn( modifier = Modifier.applyBlurSource(blur), contentPadding = paddingValues @@ -260,10 +266,16 @@ fun DetailsInfo( item { Button( onClick = onWatchClicked, - modifier = Modifier.fillMaxWidth().padding(8.dp).focusRequester(requester) - ) { Text(stringResource(Res.string.watch)) } - LaunchedEffect(null) { - withFrameNanos {} + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .focusRequester(requester) + ) { + Text(stringResource(Res.string.watch)) + } + + // Чистый фокус без сдвига кадров через withFrameNanos + LaunchedEffect(Unit) { requester.requestFocus() } } @@ -275,7 +287,13 @@ private fun OverviewDescription( details: VideoDetails, showDetails: MutableState ) { - Column(modifier = Modifier.fillMaxWidth().padding(4.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + // Самая важная магия: плавно анимирует высоту колонки при изменении maxLines текста + .animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)) + ) { Text( text = stringResource(Res.string.desc), color = MaterialTheme.colorScheme.primary, @@ -288,17 +306,21 @@ private fun OverviewDescription( maxLines = if (showDetails.value) Int.MAX_VALUE else 3, overflow = TextOverflow.Ellipsis ) - if (!showDetails.value) { - val textRes = if (details.isMovie) { - Res.string.details_movie - } else { - Res.string.details_serial - } + + // Вместо резкого 'if' плавно убираем кнопку разворачивания + AnimatedVisibility( + visible = !showDetails.value, + enter = fadeIn(), + exit = fadeOut() + ) { + val textRes = if (details.isMovie) Res.string.details_movie else Res.string.details_serial Text( text = stringResource(textRes), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.clickable { showDetails.value = true } + modifier = Modifier + .padding(top = 8.dp) // Небольшой отступ сверху от основного текста + .clickable { showDetails.value = true } ) } } @@ -311,8 +333,10 @@ private fun OverviewTitle(details: VideoDetails) { painter = rememberImagePainter(details.poster), contentDescription = null, modifier = Modifier - .clip(RoundedCornerShape(8)), + .padding(8.dp) // Добавим легкий отступ, чтобы картинка не липла к краям экрана + .clip(RoundedCornerShape(12.dp)), // На современных девайсах 12.dp выглядит аккуратнее, чем 8% contentScale = ContentScale.Crop, ) } } + diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt index 0269972..4bbe94c 100644 --- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt +++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt @@ -1,17 +1,19 @@ package ru.shadowsparky.ui.components +import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -24,14 +26,18 @@ private annotation class ExpressiveListDsl internal data class ExpressiveNode( val key: Any?, val contentType: Any?, - val content: @Composable () -> Unit + val content: @Composable LazyItemScope.() -> Unit +) + +internal data class ExpressiveSection( + val nodes: List ) @ExpressiveListDsl class ExpressiveSectionScope { internal val nodes = mutableListOf() - fun item(key: Any? = null, contentType: Any? = null, content: @Composable () -> Unit) { + fun item(key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.() -> Unit) { nodes += ExpressiveNode(key, contentType, content) } @@ -39,7 +45,7 @@ class ExpressiveSectionScope { items: List, key: ((T) -> Any)? = null, contentType: ((T) -> Any)? = null, - itemContent: @Composable (T) -> Unit + itemContent: @Composable LazyItemScope.(T) -> Unit ) { itemsIndexed(items, key, contentType) { _, t -> itemContent(t) } } @@ -48,7 +54,7 @@ class ExpressiveSectionScope { items: List, key: ((T) -> Any)? = null, contentType: ((T) -> Any)? = null, - itemContent: @Composable (Int, T) -> Unit + itemContent: @Composable LazyItemScope.(Int, T) -> Unit ) { items.forEachIndexed { index, t -> nodes += ExpressiveNode( @@ -62,11 +68,13 @@ class ExpressiveSectionScope { @ExpressiveListDsl class ExpressiveLazyListScope { - internal val sections = mutableListOf() + internal val sections = mutableListOf() fun section(content: ExpressiveSectionScope.() -> Unit) { val s = ExpressiveSectionScope().apply(content) - if (s.nodes.isNotEmpty()) sections += s + if (s.nodes.isNotEmpty()) { + sections += ExpressiveSection(s.nodes) + } } } @@ -96,7 +104,11 @@ fun ExpressiveLazyColumn( if (sectionIndex != 0 && sectionSpacing > 0.dp) { item(key = "sectionSpacer_$sectionIndex") { - Spacer(Modifier.height(sectionSpacing)) + Spacer( + Modifier + .animateItem() + .height(sectionSpacing) + ) } } @@ -106,12 +118,18 @@ fun ExpressiveLazyColumn( item(key = node.key, contentType = node.contentType) { ExpressiveCard( - topRadius, - bottomRadius, - horizontalPadding, - verticalSpacing, - content = node.content - ) + topRadius = topRadius, + bottomRadius = bottomRadius, + horizontalPadding = horizontalPadding, + verticalSpacing = verticalSpacing, + modifier = Modifier.animateItem( + fadeInSpec = spring(), + fadeOutSpec = spring(), + placementSpec = spring() + ) + ) { + node.content(this@item) + } } } } @@ -140,23 +158,3 @@ fun ExpressiveCard( modifier = modifier.padding(horizontal = horizontalPadding, vertical = verticalSpacing) ) { content() } } - -@Composable -@Preview -private fun ExpressiveLazyColumnPreview() { - ExpressiveLazyColumn { - section { - item { - TextPreference(text = "item one") - } - item { - TextPreference(text = "item two") - } - } - section { - item { - TextPreference(text = "next section started") - } - } - } -}