mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +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
+1
@@ -67,6 +67,7 @@ fun DesktopNoticeView(
|
||||
title = stringResource(R.string.screen_link_new_device_desktop_title, appName),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||
modifier = modifier,
|
||||
isScrollable = true,
|
||||
buttons = {
|
||||
Button(
|
||||
text = stringResource(R.string.screen_link_new_device_desktop_submit),
|
||||
|
||||
+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()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user