mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Try making HeaderFooterView more robust (#7267)
* Try making `HeaderFooterView` more robust Create `HeaderFooterLayout` to properly calculate the content size when the component is scrollable so it's at least the size of the available space. * Make `DesktopNoticeView` scrollable For this, `NumberedListOrganism` can't be a `LazyColumn`, since that's a lazy layout with no intrinsic height and Compose doesn't know how to size it. * Fix the case where no header or content composables are supplied * Fix insets in non-scrollable UI * Remove `movableContentOf`, suppress the warning instead since the `isScrollable` parameter should never toggle this behavior * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
77edfec61a
commit
cc3c68411e
+3
-4
@@ -9,8 +9,7 @@
|
||||
package io.element.android.libraries.designsystem.atomic.organisms
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -23,11 +22,11 @@ fun NumberedListOrganism(
|
||||
items: ImmutableList<AnnotatedString>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
itemsIndexed(items) { index, item ->
|
||||
for ((index, item) in items.withIndex()) {
|
||||
NumberedListMolecule(index = index + 1, text = item)
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* A layout with a [header], a [footer] and a [content] composables.
|
||||
* The component can be scrollable or not, which will modify which layout is used internally to arrange the UI.
|
||||
* In both cases, the [content] will use all the available space between the [header] and the [footer].
|
||||
* The [footer] is always visible, the [header] and the [content] can be scrolled if [isScrollable] is true.
|
||||
*
|
||||
* @param header the header composable.
|
||||
* @param footer the footer composable.
|
||||
* @param isScrollable if the content should be scrollable.
|
||||
* @param contentInsetsPadding padding values to apply to the content. This is useful to handle system insets.
|
||||
* @param modifier Compose modifier.
|
||||
* @param content the main content composable.
|
||||
*/
|
||||
@Suppress("ContentSlotReused")
|
||||
@Composable
|
||||
fun HeaderFooterLayout(
|
||||
header: @Composable () -> Unit,
|
||||
footer: @Composable () -> Unit,
|
||||
isScrollable: Boolean,
|
||||
contentInsetsPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
if (isScrollable) {
|
||||
// This is a hack to make the content at least 1px high, otherwise the layout will crash
|
||||
var newHeight by remember { mutableIntStateOf(1) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(contentInsetsPadding)
|
||||
.weight(1f, fill = true)
|
||||
.onSizeChanged {
|
||||
newHeight = it.height
|
||||
}
|
||||
) {
|
||||
Layout(
|
||||
content = {
|
||||
header()
|
||||
content()
|
||||
},
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
measurePolicy = { measurables, constraints ->
|
||||
val actualConstraints = constraints.copy(minWidth = 0, minHeight = 0, maxHeight = newHeight)
|
||||
val headerPlaceable = measurables.firstOrNull()?.measure(actualConstraints)
|
||||
|
||||
val contentPlaceable = if (headerPlaceable != null && measurables.size > 1) {
|
||||
val availableContentHeight = max(1, actualConstraints.maxHeight - headerPlaceable.height)
|
||||
val contentConstraints = actualConstraints.copy(minHeight = availableContentHeight, maxHeight = Constraints.Infinity)
|
||||
measurables[1].measure(contentConstraints)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val headerHeight = headerPlaceable?.height ?: 0
|
||||
val contentHeight = contentPlaceable?.height ?: 0
|
||||
layout(actualConstraints.maxWidth, headerHeight + contentHeight) {
|
||||
var yPosition = 0
|
||||
|
||||
headerPlaceable?.let {
|
||||
it.placeRelative(0, yPosition)
|
||||
yPosition += it.height
|
||||
}
|
||||
|
||||
contentPlaceable?.placeRelative(0, yPosition)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(contentInsetsPadding)) {
|
||||
header()
|
||||
}
|
||||
Box(Modifier.weight(1f, fill = true)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
footer()
|
||||
}
|
||||
}
|
||||
+15
-41
@@ -9,19 +9,14 @@
|
||||
package io.element.android.libraries.designsystem.atomic.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -82,48 +77,27 @@ fun HeaderFooterPage(
|
||||
Box {
|
||||
background()
|
||||
|
||||
// Render in a Column
|
||||
Column(
|
||||
HeaderFooterLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues = contentPadding)
|
||||
.consumeWindowInsets(insetsPadding)
|
||||
.imePadding(),
|
||||
) {
|
||||
// Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.run {
|
||||
if (isScrollable) {
|
||||
verticalScroll(rememberScrollState())
|
||||
// Make sure the scrollable content takes the full available height
|
||||
.height(IntrinsicSize.Max)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
}
|
||||
// Apply insets here so if the content is scrollable it can get below the top app bar if needed
|
||||
.padding(contentInsetsPadding)
|
||||
.weight(1f, fill = true),
|
||||
) {
|
||||
// Header
|
||||
header()
|
||||
Box {
|
||||
content()
|
||||
isScrollable = isScrollable,
|
||||
contentInsetsPadding = contentInsetsPadding,
|
||||
header = header,
|
||||
content = content,
|
||||
footer = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(footerInsetsPadding)
|
||||
) {
|
||||
footer()
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(footerInsetsPadding)
|
||||
) {
|
||||
footer()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user