Created
August 7, 2025 18:29
-
-
Save ardakazanci/597337b56f03516bbe744412a7f279cf to your computer and use it in GitHub Desktop.
Jetpack Compose Parallax Concept
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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