Zoltan Demant
07/11/2023, 10:31 AMTimo Drick
07/11/2023, 11:20 AMZoltan Demant
07/11/2023, 11:44 AMZoltan Demant
07/11/2023, 1:06 PMModifier.padding(x)
and onGloballyPositioned
not playing well together in the same layout.
I have tried using a custom layout and while the results were better, I couldnt find a way to ensure that other elements dont overlap this item (probably due to placeable.place
with offset) though.
Im not sure where to take it from here. If anyone has any ideas, please give me a poke! 💡
Layout variant (correct offset, but other items in the list overlap by 'totto'):
private fun Modifier.p(
paddingValues: PaddingValues,
): Modifier {
return composed {
var offset by remember {
mutableIntStateOf(0)
}
onGloballyPositioned { coordinates ->
val windowToLocal = coordinates.windowToLocal(Zero)
offset = windowToLocal.y.toInt()
}.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val totto = (offset + paddingValues.calculateTopPadding().roundToPx()).let { offset ->
if (offset >= 0) offset else 0
}
layout(
width = placeable.width,
height = placeable.height + totto,
) {
placeable.place(
IntOffset(
0,
totto,
),
)
}
}
}
}
OnGloballyPositioned stuff (numbers seem correct, but the padding isnt):
@Composable
fun Header(
modifier: Modifier = Modifier,
title: String,
secondary: @Composable (() -> Unit)? = null,
) {
val topPadding = with(LocalDensity.current) {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx()
}
var extraTopPadding by remember {
mutableStateOf(0.dp)
}
Surface(
modifier = modifier.onGloballyPositioned { coordinates ->
val offset = coordinates.positionInRoot().run {
y - topPadding
}
logcat { "Offset: $offset" }
extraTopPadding = when {
offset <= 0 -> offset.absoluteValue
else -> 0f
}.dp
},
color = Theme.palette.background,
content = {
Item(
modifier = Modifier.padding(
top = extraTopPadding,
),
content = {
Text(
text = title,
style = Theme.type.title,
variant = Bold,
)
},
trailing = {
secondary?.invoke()
},
)
},
)
}
Alex Vanyo
07/11/2023, 9:59 PM150.dp
for the LazyColumn
, does the sticky header still overlap with that padding? If so, that definitely seems like a bug for how sticky headers workZoltan Demant
07/12/2023, 5:54 AMTimo Drick
07/12/2023, 8:22 AMTimo Drick
07/12/2023, 8:23 AMTimo Drick
07/12/2023, 8:42 AMZoltan Demant
07/12/2023, 9:32 AMTimo Drick
07/12/2023, 9:34 AMTimo Drick
07/12/2023, 9:38 AMTimo Drick
07/12/2023, 10:47 AMZoltan Demant
09/27/2023, 8:42 AMLazyColumn.stickyHeader
item take WindowInsets.statusBars
into account whenever it is stickied (in my case, thats when it overlaps the statusBar)? I can calculate its sticky state, but adding the statusBars padding "as is" to the item scrolls the list up/down, and makes for a pretty horrible experience, regardless of it being animated or not!Rafs
09/27/2023, 9:52 AMWindowInset
library, you can calculate the padding of the status bar and apply it to your LazyColumn
Zoltan Demant
09/27/2023, 10:13 AMgsala
09/27/2023, 1:55 PMgsala
09/27/2023, 1:57 PMRafs
09/27/2023, 1:58 PMZoltan Demant
09/27/2023, 2:35 PMgsala
09/27/2023, 2:47 PMgsala
09/27/2023, 2:49 PMstickyHeader
copy it and adjustTimo Drick
09/29/2023, 1:36 PMTimo Drick
09/29/2023, 1:37 PMStylianos Gakis
09/29/2023, 1:38 PMTimo Drick
09/29/2023, 1:40 PMTimo Drick
09/29/2023, 1:41 PMTimo Drick
09/29/2023, 1:41 PMStylianos Gakis
09/29/2023, 1:42 PMTimo Drick
09/29/2023, 1:43 PMTimo Drick
09/29/2023, 1:44 PMStylianos Gakis
09/29/2023, 1:46 PMStylianos Gakis
09/29/2023, 1:47 PMTimo Drick
09/29/2023, 1:47 PMStylianos Gakis
09/29/2023, 1:48 PMTimo Drick
09/29/2023, 1:49 PMTimo Drick
09/29/2023, 1:49 PMTimo Drick
09/29/2023, 1:50 PMTimo Drick
09/29/2023, 1:50 PMZoltan Demant
09/29/2023, 2:59 PMOr maybe you could implement a composable which monitors its current position and applys padding dynamically.This works, but as soon as you start adding the padding to the item, the list scrolls unexpectedly (to the user). Ive even tried just translating the item to dodge the statusBars, but then theres just a hole where you see the underlying items scroll through 😅
Or apply insets to all sticky headers, but that's gonna look jarring too, in a different way.Unfortunately yes, I think just applying
Modifier.padding(x)
to the LazyColumn itself is the best workaround for now. You dont get content scrolling undeneath the statusBar, but all other options just look worse imo.Zoltan Demant
09/29/2023, 3:01 PMWindowInsets.padding * floatingFraction
and perhaps, just maybe, it wont make the LazyColumn scroll oddly.
@Composable
override fun floating(
key: Any,
): State<Boolean> {
return remember(state) {
derivedStateOf {
state.layoutInfo.visibleItemsInfo.matchFloating { info ->
info.key == key
}
}
}
}
private inline fun List<LazyListItemInfo>.matchFloating(
accept: (LazyListItemInfo) -> Boolean,
): Boolean {
if (size >= 2) {
val current = this[0]
if (accept(current)) {
val next = this[1]
return current.size + current.offset >= next.offset
}
}
return false
}
Timo Drick
09/30/2023, 9:50 AMTimo Drick
09/30/2023, 9:50 AM@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StickHeaderIssue() {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = WindowInsets.systemBars.asPaddingValues(),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
stickyHeader {
Text("Sticky Header 1", Modifier.locationAwareInsetsOffset(WindowInsets.systemBars, "Header 1"))
}
items(5) { index ->
Text(text = "Item $index")
}
stickyHeader {
Text("Sticky Header 2", Modifier.locationAwareInsetsOffset(WindowInsets.systemBars, "Header 2"))
}
items(30) { index ->
Text(text = "Item $index")
}
}
}
@Stable
fun Modifier.locationAwareInsetsOffset(insets: WindowInsets, label: String): Modifier = this.then(
LocationAwareInsetsOffsetModifier(insets, label)
)
internal class LocationAwareInsetsOffsetModifier(
private val insets: WindowInsets,
private val label: String
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val insetsTop = insets.getTop(this)
val placeable = measurable.measure(constraints)
val width = constraints.constrainWidth(placeable.width)
val height = constraints.constrainHeight(placeable.height)
return layout(width, height) {
val posY = coordinates?.localToWindow(Offset.Zero)?.let { pos ->
pos.y.toInt()
} ?: 0
val top = (insetsTop - posY).coerceIn(0, insetsTop)
log("$label - insets: $insetsTop topOffset: $top")
placeable.place(0, top)
}
}
}
Timo Drick
09/30/2023, 9:50 AMZoltan Demant
09/30/2023, 10:00 AMTimo Drick
09/30/2023, 10:03 AMTimo Drick
09/30/2023, 10:03 AMTimo Drick
09/30/2023, 10:07 AMTimo Drick
09/30/2023, 10:07 AMTimo Drick
09/30/2023, 10:08 AMTimo Drick
09/30/2023, 10:10 AMZoltan Demant
09/30/2023, 10:12 AMTimo Drick
09/30/2023, 10:13 AMTimo Drick
09/30/2023, 10:14 AMZoltan Demant
09/30/2023, 10:15 AMTimo Drick
09/30/2023, 10:15 AMTimo Drick
09/30/2023, 1:18 PMTimo Drick
09/30/2023, 1:18 PM@Composable
fun StickHeaderIssue() {
val listState = rememberLazyListState()
LazyColumn(
modifier = Modifier.fillMaxWidth(),//.windowInsetsPadding(WindowInsets.systemBars),
state = listState,
contentPadding = WindowInsets.systemBars.asPaddingValues(),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
items(5) { index ->
Text(text = "Item $index")
}
stickyHeaderContentPaddingAware(listState, key = "A") {
Box(
modifier = Modifier.background(Color.Blue.copy(alpha = 0.5f))) {
Text(
"Sticky Header 1"
)
}
}
items(5) { index ->
Text(text = "Item $index")
}
stickyHeaderContentPaddingAware(listState, key = "B") {
Box(modifier = Modifier.background(Color.Blue.copy(alpha = 0.5f))) {
Text(
"Sticky Header 2"
)
}
}
items(30) { index ->
Text(text = "Item $index")
}
}
}
private data class StickyType(val contentType: Any?)
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.stickyHeaderContentPaddingAware(
listState: LazyListState,
key: Any,
contentType: Any? = null,
content: @Composable LazyItemScope.() -> Unit
) {
stickyHeader(
key = key,
contentType = StickyType(contentType),
content = {
Layout(content = { content() }) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
val width = constraints.constrainWidth(placeable.width)
val height = constraints.constrainHeight(placeable.height)
layout(width, height) {
val posY = coordinates?.localToWindow(Offset.Zero)?.let { pos ->
pos.y.toInt()
} ?: 0
val paddingTop = listState.layoutInfo.beforeContentPadding
var top = (paddingTop - posY).coerceIn(0, paddingTop)
if (top > 0) {
val second = listState.layoutInfo.visibleItemsInfo
.filter { it.contentType is StickyType }
.getOrNull(1)
if (second != null && second.key != key) {
val secondOffset = second.offset
if (secondOffset <= height) {
top -= (height - secondOffset)
}
}
}
placeable.place(0, top)
}
}
}
)
}
Zoltan Demant
09/30/2023, 1:31 PMTimo Drick
09/30/2023, 1:34 PMZoltan Demant
09/30/2023, 1:48 PMTimo Drick
09/30/2023, 1:50 PMval posY = coordinates?.localToWindow(Offset.Zero)?.let { pos ->
pos.y.toInt()
} ?: 0
window coordinates.Timo Drick
09/30/2023, 1:51 PMval posY = coordinates?.positionInParent()?.y?.toInt() ?: 0
Now it should be general i hope 😄Zoltan Demant
09/30/2023, 1:51 PMTimo Drick
09/30/2023, 1:51 PMprivate data class StickyType(val contentType: Any?)
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.stickyHeaderContentPaddingAware(
listState: LazyListState,
key: Any,
contentType: Any? = null,
content: @Composable LazyItemScope.() -> Unit
) {
stickyHeader(
key = key,
contentType = StickyType(contentType),
content = {
Layout(content = { content() }) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
val width = constraints.constrainWidth(placeable.width)
val height = constraints.constrainHeight(placeable.height)
layout(width, height) {
val posY = coordinates?.positionInParent()?.y?.toInt() ?: 0
val paddingTop = listState.layoutInfo.beforeContentPadding
log("Viewport start offset: ${listState.layoutInfo.viewportStartOffset} padding: $paddingTop")
var top = (paddingTop - posY).coerceIn(0, paddingTop)
if (top > 0) {
val second = listState.layoutInfo.visibleItemsInfo
.filter { it.contentType is StickyType }
.getOrNull(1)
if (second != null && second.key != key) {
val secondOffset = second.offset
if (secondOffset <= height) {
top -= (height - secondOffset)
}
}
}
placeable.place(0, top)
}
}
}
)
}
Zoltan Demant
09/30/2023, 1:52 PMTimo Drick
09/30/2023, 1:52 PMTimo Drick
09/30/2023, 1:53 PMZoltan Demant
10/18/2023, 3:22 AMval statusBarsPadding = WindowInsets.statusBars.asPaddingValues()
val topPadding by transitionDp(
if (active) {
statusBarsPadding.calculateTopPadding()
} else {
Zero
},
)
Surface(
modifier = modifier.offset(y = topPadding),
elevation = elevation,
color = color,
content = content,
)
Box(Modifier.background(color).requiredHeight(topPadding).fillMaxWidth())