Skip to content

Instantly share code, notes, and snippets.

@mxalbert1996
Created December 8, 2022 10:18
Show Gist options
  • Save mxalbert1996/adc223d55d7ad99843265a716fc27878 to your computer and use it in GitHub Desktop.
Save mxalbert1996/adc223d55d7ad99843265a716fc27878 to your computer and use it in GitHub Desktop.
A layout that allows content to flow over header and footer when scrolled.
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* A layout that allow [content] to flow over [header] and [footer] when scrolled.
*/
@Composable
fun HeaderFooterLayout(
header: @Composable () -> Unit,
footer: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
var headerHeight by remember { mutableStateOf(0f) }
var footerHeight by remember { mutableStateOf(0f) }
var offset by rememberSaveable { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
if (available.y < 0 && offset > -headerHeight) {
val toConsume = available.y.coerceAtLeast(-headerHeight - offset)
offset += toConsume
Offset(0f, toConsume)
} else if (available.y > 0 && offset < -headerHeight) {
val toConsume = available.y.coerceAtMost(-headerHeight - offset)
offset += toConsume
Offset(0f, toConsume)
} else {
Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
val toConsume = available.y.coerceIn(-headerHeight - footerHeight - offset, -offset)
offset += toConsume
return Offset(0f, toConsume)
}
}
}
Layout(
content = {
Box { header() }
Box { footer() }
content()
},
modifier = modifier.nestedScroll(nestedScrollConnection)
) { measurables, constraints ->
val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val headerPlaceable = measurables[0].measure(relaxedConstraints)
val footerPlaceable = measurables[1].measure(relaxedConstraints)
headerHeight = headerPlaceable.height.toFloat()
footerHeight = footerPlaceable.height.toFloat()
val minOffset = (-headerPlaceable.height - footerPlaceable.height).toFloat()
Snapshot.withoutReadObservation {
if (offset < minOffset) offset = minOffset
}
val contentConstraints = relaxedConstraints.copy(minHeight = constraints.maxHeight)
val placeables = measurables.subList(2, measurables.size)
.map { it.measure(contentConstraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
headerPlaceable.placeRelative(0, 0)
footerPlaceable.placeRelative(0, constraints.maxHeight - footerPlaceable.height)
placeables.forEach {
it.placeRelativeWithLayer(0, headerPlaceable.height) {
translationY = offset
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun HeaderFooterLayoutPreview() {
MaterialTheme {
HeaderFooterLayout(
header = {
Text(
text = "Header",
modifier = Modifier
.clickable {}
.fillMaxWidth()
.height(50.dp)
.wrapContentSize()
)
},
footer = {
Text(
text = "Footer",
modifier = Modifier
.clickable {}
.fillMaxWidth()
.height(50.dp)
.wrapContentSize()
)
}
) {
Card(
elevation = 2.dp,
modifier = Modifier.padding(horizontal = 16.dp)
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
repeat(20) { i ->
@OptIn(ExperimentalMaterialApi::class)
ListItem(
text = { Text(text = "Item ${i + 1}") },
modifier = Modifier.clickable {}
)
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment