Hey guys. I'm inventing some sort of custom collap...
# compose
p
Hey guys. I'm inventing some sort of custom collapsable header and I have some issues and questions. Please see attached video. Code in 🧵 1. If you look at attached video, my scrollable content (text in red area) keeps scrolling, while header is scrolling. How can I prevent that? I've tried consuming text scroll events, until my header is scrolling, but I've not been able to implement it. I think the most annoying part is, that my header height is not constant. 2. Did I reinvent the wheel with my collapsable header implementation? Are there any better ways to do it?
Copy code
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
import com.my.app.R

@Composable
fun CollapsableHeader(
    scrollPosition: Int,
    topContent: @Composable (() -> Unit),
    titleContent: @Composable (() -> Unit),
    backgroundContent: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    collapsableContent: (@Composable () -> Unit)? = {},
    bottomContent: (@Composable () -> Unit)? = {},
) {
    Box(modifier = modifier) {
        Background(backgroundContent = backgroundContent, modifier = Modifier.matchParentSize())

        Foreground(
            scrollPosition = scrollPosition,
            topContent = topContent,
            titleContent = titleContent,
            collapsableContent = collapsableContent,
            bottomContent = bottomContent,
        )
    }
}

@Composable
private fun Background(backgroundContent: @Composable () -> Unit, modifier: Modifier = Modifier) {
    Box(modifier = modifier) {
        backgroundContent()
    }
}

@Composable
private fun Foreground(
    scrollPosition: Int,
    topContent: @Composable (() -> Unit),
    titleContent: @Composable (() -> Unit),
    collapsableContent: (@Composable () -> Unit)? = null,
    bottomContent: (@Composable () -> Unit)? = null,
) {
    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimary) {
        Layout(
            content = {
                Box(modifier = Modifier.statusBarsPadding()) {
                    topContent()
                }

                ProvideTextStyle(MaterialTheme.typography.titleLarge) {
                    titleContent()
                }

                Box {
                    collapsableContent?.invoke()
                }

                Box {
                    bottomContent?.invoke()
                }
            },
        ) { measurables, constraints ->
            val top = measurables[0].measure(constraints)
            val title = measurables[1].measure(constraints)
            val collapsable = measurables[2].measure(constraints)
            val bottom = measurables[3].measure(constraints)

            val screenWidth = constraints.maxWidth
            val nodesHeight = top.height + title.height + bottom.height
            val paddingHeight = (screenWidth - nodesHeight - collapsable.height).coerceAtLeast(0)
            val titleTopPadding = (paddingHeight - scrollPosition).coerceAtLeast(0)

            val collapsableAlpha = (1f - scrollPosition.toFloat() / paddingHeight).coerceIn(0f, 1f)
            val collapsableHeight = if (titleTopPadding > 0) {
                collapsable.height
            } else {
                (collapsable.height - scrollPosition + paddingHeight).coerceAtLeast(0)
            }

            val height = nodesHeight + titleTopPadding + collapsableHeight

            layout(width = screenWidth, height = height) {
                var currentY = 0

                top.place(x = 0, y = 0)
                currentY += titleTopPadding + top.height

                title.place(x = 0, y = currentY)
                currentY += title.height

                if (collapsableHeight > 0) {
                    collapsable.placeWithLayer(x = 0, y = currentY) { alpha = collapsableAlpha }
                    currentY += collapsableHeight
                }

                bottom.place(x = 0, y = currentY)
            }
        }
    }
}

@Composable
@Preview
private fun PreviewBehavior() {
    Surface {
        val scroll = rememberScrollState()
        Column {
            CollapsableHeader(
                scrollPosition = scroll.value,
                topContent = { BackArrow(onBackClick = { }) },
                titleContent = { Text(loremIpsum(7)) },
                collapsableContent = { Text(loremIpsum(7)) },
                bottomContent = { Text(loremIpsum(7)) },
                backgroundContent = {
                    Image(
                        painter = painterResource(id = R.drawable.bg_something),
                        contentScale = ContentScale.Crop,
                        contentDescription = null,
                        modifier = Modifier.fillMaxSize(),
                    )
                },
            )

            Text(
                text = loremIpsum(100),
                modifier = Modifier
                    .verticalScroll(scroll)
                    .height(2000.dp)
                    .fillMaxWidth()
                    .background(Color.Red),
            )
        }
    }
}

fun loremIpsum(words: Int): String {
    return LoremIpsum(words).values.first()
}