What You Will Build

A scrollable list with a gradient hero header that moves at half the scroll speed, creating a depth parallax effect. The header also fades out as the user scrolls down. This pattern is used in profile pages, landing screens, and content detail views.

Why This Pattern Matters

Parallax scrolling creates a sense of depth and polish that distinguishes professional apps. Learning to read scroll offset from LazyListState and apply it to graphicsLayer transformations is a technique that applies to collapsing toolbars, sticky headers, and scroll-driven animations.

Key Concepts

  • rememberLazyListState for accessing scroll position.
  • graphicsLayer translationY for parallax offset at 0.5x speed.
  • graphicsLayer alpha for scroll-driven fade-out.
  • firstVisibleItemScrollOffset for pixel-precise scroll tracking.

The Parallax Screen

@Composable
fun ScrollParallaxEffectScreen() {
    val listState = rememberLazyListState()
    val colors = listOf(
        Color(0xFF667EEA), Color(0xFFE91E63),
        Color(0xFF4CAF50), Color(0xFFFF9800), Color(0xFF9C27B0)
    )

    LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
        item {
            val offset = if (listState.firstVisibleItemIndex == 0)
                listState.firstVisibleItemScrollOffset.toFloat()
            else 300f

            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(300.dp)
                    .graphicsLayer {
                        translationY = offset * 0.5f
                        alpha = 1f - (offset / 600f)
                    }
                    .background(
                        Brush.verticalGradient(
                            listOf(Color(0xFF667EEA), Color(0xFF764BA2))
                        )
                    ),
                contentAlignment = Alignment.Center
            ) {
                Text("Scroll Parallax",
                    fontSize = 32.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.White)
            }
        }
        items(20) { index ->
            Card(
                modifier = Modifier.fillMaxWidth()
                    .padding(horizontal = 16.dp, vertical = 6.dp),
                colors = CardDefaults.cardColors(
                    containerColor = colors[index % colors.size]
                        .copy(alpha = 0.1f)
                )
            ) {
                Text("Item ${'$'}{index + 1}",
                    modifier = Modifier.padding(16.dp),
                    fontSize = 16.sp)
            }
        }
    }
}

Tips and Pitfalls

  • 0.5f multiplier: translationY = offset * 0.5f makes the header move at half the scroll speed, creating the parallax depth illusion.
  • Guard with firstVisibleItemIndex: Once the header scrolls past, use a fixed value (300f) to prevent calculation errors.
  • Alpha fade formula: 1f - (offset / 600f) creates a gradual fade that reaches zero after 600px of scrolling.
  • Image parallax: Replace the gradient Box with an AsyncImage and apply the same graphicsLayer for photo parallax headers.

Minimum SDK: API 21+ with Compose BOM

f (tx.isIncome) Color(0xFF4CAF50)
else Color(0xFFF44336))
}
Spacer(Modifier.width(12.dp))
Text(tx.name, modifier = Modifier.weight(1f))
Text(
formattedAmount,
fontWeight = FontWeight.Bold,
color = if (tx.isIncome) Color(0xFF4CAF50)
else Color(0xFFF44336)
)
}
}

Tips and Pitfalls

  • sumOf on transaction lists computes totals declaratively. This is cleaner than maintaining separate income/expense state variables.
  • Consistent color pairs: Green (#81C784 light, #4CAF50 standard) for income, Red (#EF9A9A light, #F44336 standard) maintains visual harmony.
  • For production: Use a ViewModel with Room database to persist transactions and compute balances reactively with Flow/StateFlow.
  • Add pull-to-refresh with pullToRefresh modifier for syncing transaction data from a backend.

Minimum SDK: API 24+ with Compose BOM 2024.01+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.