Skip to content

Instantly share code, notes, and snippets.

@ardakazanci
Created August 7, 2025 18:29
Show Gist options
  • Select an option

  • Save ardakazanci/597337b56f03516bbe744412a7f279cf to your computer and use it in GitHub Desktop.

Select an option

Save ardakazanci/597337b56f03516bbe744412a7f279cf to your computer and use it in GitHub Desktop.

Revisions

  1. ardakazanci created this gist Aug 7, 2025.
    97 changes: 97 additions & 0 deletions Parallax.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,97 @@
    @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)
    )
    }
    }
    }
    }