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) as shown below

overlapping_ui_compose.jpg

We're going to build 2 boxes that overlap on each other. First box is large inner box (as cover image) while 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"
    )
}

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

Thank you.