Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save pedromassango/d1b40fe18fd9b2e48ed9ea96193f92a2 to your computer and use it in GitHub Desktop.

Select an option

Save pedromassango/d1b40fe18fd9b2e48ed9ea96193f92a2 to your computer and use it in GitHub Desktop.
NOTE: This code was not optimized to be used in production. A clean up and refactoring might be need!
import androidx.compose.animation.core.LinearEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlin.math.PI
import kotlin.math.abs
val items = listOf(
"Wakeup",
"Eat",
"Learn",
"Repeat",
)
@Composable
fun App() {
Scaffold { padding ->
Column(
Modifier
.fillMaxSize()
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.padding(bottom = 60.dp))
AnimatedHorizontalSlideShow(
items = items,
itemBuilder = { index, item ->
SlideShowCard(item)
}
)
}
}
}
@Composable
private fun SlideShowCard(
item: String
) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
item,
fontWeight = FontWeight.Bold,
color = Color.White,
fontSize = 24.sp
)
}
}
@Composable
private fun <T> AnimatedHorizontalSlideShow(
items: List<T>,
modifier: Modifier = Modifier,
cardFraction: Float = 0.58f,
arcHeight: Dp = 2.dp,
cardHeight: Dp = 400.dp,
spacing: Dp = 12.dp,
maxTranslationY: Dp = 24.dp,
itemBuilder: @Composable (index: Int, item: T) -> Unit,
) {
BoxWithConstraints(modifier = modifier) {
val containerWidthDp = maxWidth
val cardWidthDp = containerWidthDp * cardFraction
val containerWidthPx = with(LocalDensity.current) { maxWidth.toPx() }
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = Int.MAX_VALUE / 2
)
val autoScroll = remember { mutableStateOf(true) }
LaunchedEffect(autoScroll.value) {
if (autoScroll.value) {
while (true) {
listState.scrollBy(PI.toFloat())
delay(16)
}
}
}
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.collect { scrolling ->
autoScroll.value = !scrolling
}
}
LazyRow(
state = listState,
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.Bottom,
modifier = Modifier
.fillMaxWidth()
.height(cardHeight + arcHeight)
) {
items(Int.MAX_VALUE) { index ->
val item = items[index % items.size]
Box(
modifier = Modifier
.width(cardWidthDp)
.height(cardHeight + arcHeight),
contentAlignment = Alignment.BottomCenter
) {
val itemInfo =
listState.layoutInfo.visibleItemsInfo.find { it.index == index }
if (itemInfo == null) {
return@items Box {}
}
// Calculate rotation based on position
// Cards tilt away from center
val rotation = itemInfo.offset.toFloat() * .0030995512f
val progress by remember(listState.firstVisibleItemScrollOffset) {
derivedStateOf {
val itemInfo = listState.layoutInfo.visibleItemsInfo
.find { it.index == index }
if (itemInfo == null) {
return@derivedStateOf 0f
}
val containerCenter = containerWidthPx / 2f
val itemCenter = itemInfo.offset + (itemInfo.size / 2f)
val distanceFromCenter =
abs(containerCenter - (itemCenter + (spacing.value * 2)))
val activationRange = containerWidthPx
val p =
(1f - (distanceFromCenter / activationRange)).coerceIn(0f, 1f)
LinearEasing.transform(p)
}
}
val translationYValue =
-progress * with(LocalDensity.current) { maxTranslationY.toPx() }
Box(
modifier = Modifier
.width(cardWidthDp)
.height(cardHeight)
.graphicsLayer {
rotationZ = rotation
rotationX = .0030995512f
translationY = translationYValue
transformOrigin = TransformOrigin(.5f, 1f)
}
.clip(RoundedCornerShape(topEnd = 24.dp, topStart = 24.dp))
.background(
brush = Brush.verticalGradient(
startY = 645f,
colors = listOf(
Color.Black,
Color.White
)
)
),
) {
itemBuilder(index, item)
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment