update expressive lazy column

This commit is contained in:
2026-07-27 21:01:19 +03:00
parent 9915e4ddd5
commit 32667ca9f3
2 changed files with 70 additions and 48 deletions
@@ -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.user_tags
import androiddev.apps.vbox.client.shared.generated.resources.watch import androiddev.apps.vbox.client.shared.generated.resources.watch
import androidx.compose.animation.AnimatedContent 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.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -44,7 +50,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
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.draw.clip import androidx.compose.ui.draw.clip
@@ -251,6 +256,7 @@ fun DetailsInfo(
val requester = remember { FocusRequester() } val requester = remember { FocusRequester() }
val blur = LocalBlurState.current val blur = LocalBlurState.current
val showDetails = rememberSaveable { mutableStateOf(false) } val showDetails = rememberSaveable { mutableStateOf(false) }
LazyColumn( LazyColumn(
modifier = Modifier.applyBlurSource(blur), modifier = Modifier.applyBlurSource(blur),
contentPadding = paddingValues contentPadding = paddingValues
@@ -260,10 +266,16 @@ fun DetailsInfo(
item { item {
Button( Button(
onClick = onWatchClicked, onClick = onWatchClicked,
modifier = Modifier.fillMaxWidth().padding(8.dp).focusRequester(requester) modifier = Modifier
) { Text(stringResource(Res.string.watch)) } .fillMaxWidth()
LaunchedEffect(null) { .padding(8.dp)
withFrameNanos {} .focusRequester(requester)
) {
Text(stringResource(Res.string.watch))
}
// Чистый фокус без сдвига кадров через withFrameNanos
LaunchedEffect(Unit) {
requester.requestFocus() requester.requestFocus()
} }
} }
@@ -275,7 +287,13 @@ private fun OverviewDescription(
details: VideoDetails, details: VideoDetails,
showDetails: MutableState<Boolean> showDetails: MutableState<Boolean>
) { ) {
Column(modifier = Modifier.fillMaxWidth().padding(4.dp)) { Column(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
// Самая важная магия: плавно анимирует высоту колонки при изменении maxLines текста
.animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessMediumLow))
) {
Text( Text(
text = stringResource(Res.string.desc), text = stringResource(Res.string.desc),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
@@ -288,17 +306,21 @@ private fun OverviewDescription(
maxLines = if (showDetails.value) Int.MAX_VALUE else 3, maxLines = if (showDetails.value) Int.MAX_VALUE else 3,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (!showDetails.value) {
val textRes = if (details.isMovie) { // Вместо резкого 'if' плавно убираем кнопку разворачивания
Res.string.details_movie AnimatedVisibility(
} else { visible = !showDetails.value,
Res.string.details_serial enter = fadeIn(),
} exit = fadeOut()
) {
val textRes = if (details.isMovie) Res.string.details_movie else Res.string.details_serial
Text( Text(
text = stringResource(textRes), text = stringResource(textRes),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary, 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), painter = rememberImagePainter(details.poster),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8)), .padding(8.dp) // Добавим легкий отступ, чтобы картинка не липла к краям экрана
.clip(RoundedCornerShape(12.dp)), // На современных девайсах 12.dp выглядит аккуратнее, чем 8%
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
) )
} }
} }
@@ -1,17 +1,19 @@
package ru.shadowsparky.ui.components package ru.shadowsparky.ui.components
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -24,14 +26,18 @@ private annotation class ExpressiveListDsl
internal data class ExpressiveNode( internal data class ExpressiveNode(
val key: Any?, val key: Any?,
val contentType: Any?, val contentType: Any?,
val content: @Composable () -> Unit val content: @Composable LazyItemScope.() -> Unit
)
internal data class ExpressiveSection(
val nodes: List<ExpressiveNode>
) )
@ExpressiveListDsl @ExpressiveListDsl
class ExpressiveSectionScope { class ExpressiveSectionScope {
internal val nodes = mutableListOf<ExpressiveNode>() internal val nodes = mutableListOf<ExpressiveNode>()
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) nodes += ExpressiveNode(key, contentType, content)
} }
@@ -39,7 +45,7 @@ class ExpressiveSectionScope {
items: List<T>, items: List<T>,
key: ((T) -> Any)? = null, key: ((T) -> Any)? = null,
contentType: ((T) -> Any)? = null, contentType: ((T) -> Any)? = null,
itemContent: @Composable (T) -> Unit itemContent: @Composable LazyItemScope.(T) -> Unit
) { ) {
itemsIndexed(items, key, contentType) { _, t -> itemContent(t) } itemsIndexed(items, key, contentType) { _, t -> itemContent(t) }
} }
@@ -48,7 +54,7 @@ class ExpressiveSectionScope {
items: List<T>, items: List<T>,
key: ((T) -> Any)? = null, key: ((T) -> Any)? = null,
contentType: ((T) -> Any)? = null, contentType: ((T) -> Any)? = null,
itemContent: @Composable (Int, T) -> Unit itemContent: @Composable LazyItemScope.(Int, T) -> Unit
) { ) {
items.forEachIndexed { index, t -> items.forEachIndexed { index, t ->
nodes += ExpressiveNode( nodes += ExpressiveNode(
@@ -62,11 +68,13 @@ class ExpressiveSectionScope {
@ExpressiveListDsl @ExpressiveListDsl
class ExpressiveLazyListScope { class ExpressiveLazyListScope {
internal val sections = mutableListOf<ExpressiveSectionScope>() internal val sections = mutableListOf<ExpressiveSection>()
fun section(content: ExpressiveSectionScope.() -> Unit) { fun section(content: ExpressiveSectionScope.() -> Unit) {
val s = ExpressiveSectionScope().apply(content) 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) { if (sectionIndex != 0 && sectionSpacing > 0.dp) {
item(key = "sectionSpacer_$sectionIndex") { 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) { item(key = node.key, contentType = node.contentType) {
ExpressiveCard( ExpressiveCard(
topRadius, topRadius = topRadius,
bottomRadius, bottomRadius = bottomRadius,
horizontalPadding, horizontalPadding = horizontalPadding,
verticalSpacing, verticalSpacing = verticalSpacing,
content = node.content 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) modifier = modifier.padding(horizontal = horizontalPadding, vertical = verticalSpacing)
) { content() } ) { 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")
}
}
}
}