From a98515120dfe9ddd79fabb1a955862d55e189eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 27 Nov 2023 12:16:58 +0100 Subject: [PATCH] Fix background parallax with deferred reads --- .../jetsnack/ui/components/Gradient.kt | 25 +++++++- .../example/jetsnack/ui/components/Snacks.kt | 63 ++++++++++--------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt index 63037d661f..40a82d8162 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt @@ -21,12 +21,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -45,20 +47,37 @@ fun Modifier.offsetGradientBackground( colors: List, width: Float, offset: Float = 0f -) = background( +) = this then background( Brush.horizontalGradient( - colors, + colors = colors, startX = -offset, endX = width - offset, tileMode = TileMode.Mirror ) ) +fun Modifier.offsetGradientBackground( + colors: List, + width: Density.() -> Float, + offset: Density.() -> Float = { 0f } +) = this then drawBehind { + val actualOffset = offset() + + drawRect( + Brush.horizontalGradient( + colors = colors, + startX = -actualOffset, + endX = width() - actualOffset, + tileMode = TileMode.Mirror + ) + ) +} + fun Modifier.diagonalGradientBorder( colors: List, borderSize: Dp = 2.dp, shape: Shape -) = border( +) = this then border( width = borderSize, brush = Brush.linearGradient(colors), shape = shape diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt index 55b374ac90..6c9cf32bc1 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt @@ -34,7 +34,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -53,6 +53,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -67,13 +68,7 @@ import com.example.jetsnack.ui.utils.mirroringIcon private val HighlightCardWidth = 170.dp private val HighlightCardPadding = 16.dp - -// The Cards show a gradient which spans 3 cards and scrolls with parallax. -private val gradientWidth - @Composable - get() = with(LocalDensity.current) { - (3 * (HighlightCardWidth + HighlightCardPadding).toPx()) - } +private val Density.cardWidthWithPaddingPx get() = (HighlightCardWidth + HighlightCardPadding).toPx() @Composable fun SnackCollection( @@ -129,28 +124,33 @@ private fun HighlightedSnacks( onSnackClick: (Long) -> Unit, modifier: Modifier = Modifier ) { - val scroll = rememberScrollState(0) + val rowState = rememberLazyListState() + + val highlightedCardWidthPx = with(LocalDensity.current) { cardWidthWithPaddingPx } + + val scrollProvider = { + // Simple calculation of scroll distance for homogenous item types with the same width. + highlightedCardWidthPx * rowState.firstVisibleItemIndex + rowState.firstVisibleItemScrollOffset + } + val gradient = when ((index / 2) % 2) { 0 -> JetsnackTheme.colors.gradient6_1 else -> JetsnackTheme.colors.gradient6_2 } - // The Cards show a gradient which spans 3 cards and scrolls with parallax. - val gradientWidth = with(LocalDensity.current) { - (6 * (HighlightCardWidth + HighlightCardPadding).toPx()) - } + LazyRow( + state = rowState, modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(start = 24.dp, end = 24.dp) ) { itemsIndexed(snacks) { index, snack -> HighlightSnackItem( - snack, - onSnackClick, - index, - gradient, - gradientWidth, - scroll.value + snack = snack, + onSnackClick = onSnackClick, + index = index, + gradient = gradient, + scrollProvider = scrollProvider ) } } @@ -214,17 +214,13 @@ private fun HighlightSnackItem( onSnackClick: (Long) -> Unit, index: Int, gradient: List, - gradientWidth: Float, - scroll: Int, + scrollProvider: () -> Float, modifier: Modifier = Modifier ) { - val left = index * with(LocalDensity.current) { - (HighlightCardWidth + HighlightCardPadding).toPx() - } JetsnackCard( modifier = modifier .size( - width = 170.dp, + width = HighlightCardWidth, height = 250.dp ) .padding(bottom = 16.dp) @@ -239,12 +235,22 @@ private fun HighlightSnackItem( .height(160.dp) .fillMaxWidth() ) { - val gradientOffset = left - (scroll / 3f) Box( modifier = Modifier .height(100.dp) .fillMaxWidth() - .offsetGradientBackground(gradient, gradientWidth, gradientOffset) + .offsetGradientBackground( + colors = gradient, + width = { + // The Cards show a gradient which spans 6 cards and scrolls with parallax. + 6 * cardWidthWithPaddingPx + }, + offset = { + val left = index * cardWidthWithPaddingPx + val gradientOffset = left - (scrollProvider() / 3f) + gradientOffset + } + ) ) SnackImage( imageUrl = snack.imageUrl, @@ -312,8 +318,7 @@ fun SnackCardPreview() { onSnackClick = { }, index = 0, gradient = JetsnackTheme.colors.gradient6_1, - gradientWidth = gradientWidth, - scroll = 0 + scrollProvider = { 0f } ) } }