How to build overlapping layout in jetpack compose

Rajesh Hadiya
2 min readDec 15, 2022

In this article, we’re going to build a UI where contents of one composable will overlap on another (as you would usually find in social profiles where profile image overlap on cover/background image) as shown below

We’re going to build 2 boxes that overlap on each other. The first box is a large inner box (as cover image) while the second box is small outer box (as profile image).

To do so, first we’ll need to build a custom layout that calculates the size of overlapping boxes and place them accordingly.

@Composable
fun OverlappingBoxes(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val largeBox = measurables[0]
val smallBox = measurables[1]
val looseConstraints = constraints.copy(
minWidth = 0,
minHeight = 0,
)
val largePlaceable = largeBox.measure(looseConstraints)
val smallPlaceable = smallBox.measure(looseConstraints)
layout(
width = constraints.maxWidth,
height = largePlaceable.height + smallPlaceable.height / 2,
) {
largePlaceable.placeRelative(
x = 0,
y = 0,
)
smallPlaceable.placeRelative(
x = (constraints.maxWidth - smallPlaceable.width) / 2,
y = largePlaceable.height - smallPlaceable.height / 2
)
}
}
}

Now, we’ll build a composable which contains actual content of our UI and pass it to OverlappingBoxes

@Composable
fun CoverAndProfileImage(
modifier: Modifier = Modifier,
coverImage: Any?,
profileImage: Any?,
onCoverClick: () -> Unit = {},
onProfileClick: () -> Unit = {},
) {
OverlappingBoxes(modifier = modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.clickable { onCoverClick() }
) {
ImageItem(
modifier = Modifier.fillMaxSize(),
data = coverImage ?: R.drawable.default_cover_image,
contentScale = ContentScale.FillWidth
)
}

Box(
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(16.dp))
.clickable { onProfileClick() }
) {
ImageItem(
modifier = Modifier.fillMaxSize(),
data = profileImage ?: R.drawable.default_profile_image,
contentScale = ContentScale.Crop
)
}
}
}

@Composable
fun ImageItem(
modifier: Modifier,
data: Any?,
crossfadeValue: Int = 300,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.Crop
) {
coil.compose.AsyncImage(
modifier = modifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(crossfadeValue)
.build(),
contentDescription = contentDescription,
placeholder = painterResource(id = R.drawable.placeholder),
contentScale = contentScale
)
}

You can see the UI in action by using below preview configuration

@Preview
@Composable
fun CoverAndProfileImagePreview() {
CoverAndProfileImage(
coverImage = "https://9to5google.com/wp-content/uploads/sites/4/2021/02/android-jetpack-header.png",
profileImage = "https://3.bp.blogspot.com/-VVp3WvJvl84/X0Vu6EjYqDI/AAAAAAAAPjU/ZOMKiUlgfg8ok8DY8Hc-ocOvGdB0z86AgCLcBGAsYHQ/s1600/jetpack%2Bcompose%2Bicon_RGB.png"
)
}
CoverAndProfileImagePreview

If you’ve made this far, do follow me for more interesting articles on android development.

Thank you.

--

--

Rajesh Hadiya

Founder of MyStore | Talks about Kotlin, Android and Spring Boot