Skip to content

Instantly share code, notes, and snippets.

@ardakazanci
Created August 7, 2025 18:29
Show Gist options
  • Save ardakazanci/597337b56f03516bbe744412a7f279cf to your computer and use it in GitHub Desktop.
Save ardakazanci/597337b56f03516bbe744412a7f279cf to your computer and use it in GitHub Desktop.
Jetpack Compose Parallax Concept
@Composable
fun Modifier.parallaxHeader(
listState: LazyListState,
headerHeightDp: Dp,
maxStretchFactor: Float = 3.0f,
pullMultiplier: Float = 1.5f,
onHeightChanged: (Dp) -> Unit
): Modifier {
val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
val headerHeightPx = with(density) { headerHeightDp.toPx() }
val currentHeaderHeightPx = remember { Animatable(headerHeightPx) }
onHeightChanged(with(density) { currentHeaderHeightPx.value.toDp() })
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
if (delta < 0 && listState.firstVisibleItemIndex == 0 && currentHeaderHeightPx.value > headerHeightPx) {
val newHeight = (currentHeaderHeightPx.value + delta).coerceAtLeast(headerHeightPx)
val consumedDelta = newHeight - currentHeaderHeightPx.value
coroutineScope.launch { currentHeaderHeightPx.snapTo(newHeight) }
return Offset(0f, consumedDelta)
}
if (delta > 0 && listState.firstVisibleItemIndex == 0) {
val newHeight = (currentHeaderHeightPx.value + delta * pullMultiplier)
.coerceAtMost(headerHeightPx * maxStretchFactor)
coroutineScope.launch { currentHeaderHeightPx.snapTo(newHeight) }
return Offset(0f, delta)
}
return Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (currentHeaderHeightPx.value > headerHeightPx) {
currentHeaderHeightPx.animateTo(
targetValue = headerHeightPx,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
dampingRatio = Spring.DampingRatioMediumBouncy
)
)
}
return super.onPreFling(available)
}
}
}
return this.nestedScroll(nestedScrollConnection)
}
@Composable
fun ParallaxScreen() {
val listState = rememberLazyListState()
val baseHeaderHeight = 210.dp
var headerHeight by remember { mutableStateOf(baseHeaderHeight) }
Box(
modifier = Modifier
.fillMaxSize()
.parallaxHeader(
listState = listState,
headerHeightDp = baseHeaderHeight,
onHeightChanged = { headerHeight = it }
)
) {
Image(
painter = painterResource(R.drawable.poke),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.height(headerHeight)
.fillMaxWidth()
.clipToBounds()
)
LazyColumn(
state = listState,
contentPadding = PaddingValues(top = headerHeight)
) {
items(50) { index ->
Text(
text = "Item #$index",
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.background(Color.White)
.padding(16.dp)
)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment