How to build overlapping layout in Jetpack Compose

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).

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 a 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 the 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"
    )
}
Share with your friends

Leave a Reply

Your email address will not be published. Required fields are marked *