What You Will Build
A profile page with a cover photo that shrinks as you scroll, an avatar that overlaps the cover edge, follower counts, and a tweet feed below. This replicates the Twitter/X profile scrolling behavior where the header collapses dynamically based on scroll position.
Why This Pattern Matters
Collapsible profile headers maximize screen space for content while keeping the identity context visible. Learning to calculate dynamic heights from scroll offset and overlap elements with negative offsets is essential for social media apps, portfolio pages, and user profile screens.
Key Concepts
- Dynamic height calculation from LazyListState scroll offset.
- coerceAtLeast to prevent negative heights.
- Negative offset to overlap the avatar onto the cover photo edge.
The Twitter Profile Screen
@Composable
fun TwitterProfileScrollingScreen() {
val listState = rememberLazyListState()
val scrollOffset = listState.firstVisibleItemScrollOffset.toFloat()
val firstIdx = listState.firstVisibleItemIndex
val coverHeight = if (firstIdx == 0)
(200f - scrollOffset * 0.4f).coerceAtLeast(0f)
else 0f
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
// Cover photo
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(coverHeight.dp)
.background(
Brush.verticalGradient(
listOf(Color(0xFF1DA1F2), Color(0xFF0D47A1))
)
)
)
}
// Profile info section
item {
Column(modifier = Modifier.padding(16.dp)) {
Box(
modifier = Modifier
.size(80.dp)
.offset(y = (-40).dp)
.clip(CircleShape)
.background(Color.White)
.padding(3.dp)
.clip(CircleShape)
.background(Color(0xFF1DA1F2)),
contentAlignment = Alignment.Center
) {
Text("JD", fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = Color.White)
}
Text("John Doe",
fontSize = 22.sp, fontWeight = FontWeight.Bold)
Text("@johndoe",
fontSize = 14.sp, color = Color.Gray)
Spacer(modifier = Modifier.height(8.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("142 Following", fontSize = 14.sp)
Text("2.4K Followers", fontSize = 14.sp)
}
}
}
// Tweet feed
items(20) { idx ->
Card(
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Text(
"Tweet ${'$'}{idx + 1}: Sample tweet content for the profile feed.",
modifier = Modifier.padding(16.dp),
fontSize = 14.sp
)
}
}
}
}
Tips and Pitfalls
- 0.4f collapse rate: The cover shrinks at 40% of scroll speed, giving a smooth collapse rather than an abrupt disappearance.
- coerceAtLeast(0f): Prevents the height from going negative, which would cause a crash.
- White border avatar: The pattern of
.background(Color.White).padding(3.dp).clip(CircleShape).background(...)creates a white ring border around the avatar. - Negative offset trick:
offset(y = (-40).dp)pulls the avatar upward to overlap the cover photo edge, a signature social media profile pattern.
Minimum SDK: API 21+ with Compose BOM