update expressive lazy column
This commit is contained in:
+38
-14
@@ -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<Boolean>
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ExpressiveNode>
|
||||
)
|
||||
|
||||
@ExpressiveListDsl
|
||||
class ExpressiveSectionScope {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -39,7 +45,7 @@ class ExpressiveSectionScope {
|
||||
items: List<T>,
|
||||
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<T>,
|
||||
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<ExpressiveSectionScope>()
|
||||
internal val sections = mutableListOf<ExpressiveSection>()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user