Dirk Hoffmann
05/17/2021, 8:14 PM@Composable
fun VanillaRootContent() {
val floatingPopup: MutableState<Boolean> = mutableStateOf(true)
// Card(Modifier.size(350.dp, 175.dp)) {
Column(
Modifier.border(1.dp, MaterialTheme.colors.onBackground).background(MaterialTheme.colors.secondary)
) {
Footer(floatingPopup)
val verticalScrollState = rememberScrollState(0)
Box(Modifier.fillMaxWidth()) { // <-- remove
Column(modifier = Modifier.verticalScroll(verticalScrollState)) {
for (i in 1..16) {
Text("LAST$i", style = MaterialTheme.typography.body2)
}
}
VerticalScrollbar( // <-- remove
adapter = rememberScrollbarAdapter(verticalScrollState),
modifier = Modifier.align(Alignment.CenterEnd),
style = LocalScrollbarStyle.current.copy(unhoverColor = Color.Gray.copy(alpha = 0.5F))
)
} // <-- remove
Footer(floatingPopup)
}
// }
}
@Composable
private fun Footer(floatingPopup: MutableState<Boolean>) {
Row(Modifier.border(2.dp, Color.LightGray), verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.weight(1f))
Text("floatingWithCursor", Modifier.padding(2.dp), style = MaterialTheme.typography.body2)
Checkbox(
floatingPopup.value,
onCheckedChange = { floatingPopup.value = !floatingPopup.value },
colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.onBackground)
)
}
}
Dirk Hoffmann
05/18/2021, 7:25 AMIgor Demin
05/18/2021, 9:34 AMVerticalScrollbar
occupies all available space.
Should the height of scrollable list also occupy all available space or should it be dynamic?
occupy all available spaceIn this case you can just use
Box(Modifier.fillMaxWidth().weight(1f))
Dirk Hoffmann
05/18/2021, 9:43 AMweight(1f)
says the box will eat all vertical available space after measuring.
If VerticalScrollbar
does eat all available space as you said, wouldn't that be the same??
As the Box will be "as high" as the components in it requireIgor Demin
05/18/2021, 9:51 AMbut I don't really understand ...says the box will eat all vertical available space after measuring.weight(1f)
weight
modifier only affects components in Column (VerticalScrollbar is not in Column). It tells how much free space component should occupy. "free space" = "all space" - "space occupied by unweighted components"
As the Box will be "as high" as the components in it requireNo, it will be as high as the height of the parent minus height of the footer/header. If you need the height of the Box to be depended on the child items, you need another approach.
Dirk Hoffmann
05/18/2021, 9:53 AMIgor Demin
05/18/2021, 10:02 AMcould you sketch that "other approach"Probably it is difficult to implement. You will need a custom layout:
Layout({
Column(...)
VerticalScrollbar(...)
}) { measureables, constraints ->
val list = measureables[0].measure(constraints)
val scrollbar = measureables[1].measure(Constraints.fixed(constraints.maxWidth, list.height))
layout(list.width, list.height) {
list.place(0, 0)
scrollbar.place(constraints.maxWidth - scrollbar.width, 0)
}
}
For placing the Footer maybe it is also needed to implement a custom layout (not sure). weight
will not work in this case.Dirk Hoffmann
05/20/2021, 12:01 PMvar contentSize by mutableStateOf(IntSize.ZERO)
Box {
Box {
Box(Modifier.onGloballyPositioned { contentSize = it.size }) {
Column(Modifier.verticalScroll(verticalScrollState)) {
for(i in 1..20) {
Text("Number${i.toString().padStart(2, '0')}")
}
}
}
}
VerticalScrollbar(
adapter = rememberScrollbarAdapter(verticalScrollState),
modifier = Modifier.height( Dp(contentSize.height / LocalDensity.current.density) )
)
}
By getting the measured content size via onGloballyPositioned
via its own Box (without the Scrollbar)
and setting the Scrollbar (in the parent Box) to that size,
it always has the height/width of the content ...Igor Demin
05/20/2021, 12:08 PMLayout
directly (if we can) instead of onGloballyPositioned
.
When we use onGloballyPositioned
we always resize on the next frame.
User can see some short-timed glitchesIgor Demin
05/20/2021, 12:10 PMsome short-timed glitchesProbably they are noticeable when we resize the window (the size of the scrollbar will be always behind the size of the content)
Dirk Hoffmann
05/20/2021, 11:34 PMsizeIn(min, max)
and the Popup having a content (which will get a scrollbar if to large) and having a footer at the bottom of it ...
Any Tutorials/Examples you can recommend beyond:
https://developer.android.com/jetpack/compose/layout#custom-layouts
and
https://developer.android.com/codelabs/jetpack-compose-layouts#6
?Dirk Hoffmann
05/20/2021, 11:35 PMIgor Demin
05/21/2021, 9:11 AMLayout
in official samples can be useful.Dirk Hoffmann
05/21/2021, 2:30 PMScaffold
does it, result:Dirk Hoffmann
05/25/2021, 4:45 PMDirk Hoffmann
05/26/2021, 9:34 AMDirk Hoffmann
05/26/2021, 9:34 AMimport androidx.compose.foundation.border
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.dp
import com.accorddesk.common.common.log.printlnErr
@Composable
fun FramedContent(
modifier: Modifier = Modifier,
topPanel: @Composable () -> Unit = {},
bottomPanel: @Composable () -> Unit = {},
leftPanel: @Composable (PaddingValues) -> Unit = {},
rightPanel: @Composable (PaddingValues) -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
mainContent: @Composable (PaddingValues) -> Unit
) {
val child = @Composable { childModifier: Modifier ->
Surface(modifier = childModifier.border(1.dp, Color.Magenta), color = backgroundColor, contentColor = contentColor) {
FramedContentLayout(
topPanel = topPanel,
bottomPanel = bottomPanel,
leftPanel = leftPanel,
rightPanel = rightPanel,
mainContent = mainContent
)
}
}
// if something to draw over the FramedContent, e.g. a ModalDrawer, it comes here
child(modifier)
}
@Composable
private fun FramedContentLayout(
topPanel: @Composable () -> Unit,
bottomPanel: @Composable () -> Unit,
leftPanel: @Composable (PaddingValues) -> Unit,
rightPanel: @Composable (PaddingValues) -> Unit,
mainContent: @Composable (PaddingValues) -> Unit
) {
SubcomposeLayout { constraints ->
val layoutMaxWidth = constraints.maxWidth
val layoutMaxHeight = constraints.maxHeight
// ==========================================================================================================+
// do some health asserts if FramedContent is placed inside a Component with INFINITE width and/or height
// ==========================================================================================================+
val unboundedInfinity = mutableListOf<String>()
if ( !constraints.hasBoundedWidth && (layoutMaxWidth >= (Int.MAX_VALUE/2) || layoutMaxWidth <= (Int.MIN_VALUE/2)) ) {
unboundedInfinity.add("unbounded width and width INFINITE")
}
if ( !constraints.hasBoundedHeight && (layoutMaxHeight >= (Int.MAX_VALUE/2) || layoutMaxHeight <= (Int.MIN_VALUE/2)) ) {
unboundedInfinity.add("unbounded height and height INFINITE")
}
if (unboundedInfinity.isNotEmpty()) {
throw Exception("FramedContent ${unboundedInfinity.joinToString()}")
}
// ==========================================================================================================+
// measure each Panel and get back Placeable's for each
// ==========================================================================================================+
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
// topPanel measuring
val topPanelPlaceables = subcompose(FramedContentLayoutContent.TopPanel, topPanel).map {
it.measure(looseConstraints)
}
val topPanelHeight = topPanelPlaceables.maxByOrNull { it.height }?.height ?: 0
val topPanelWidth = topPanelPlaceables.maxByOrNull { it.width }?.width ?: 0
// bottomPanel measuring
val bottomPanelPlaceables = subcompose(FramedContentLayoutContent.BottomPanel, bottomPanel).map {
it.measure(looseConstraints)
}
val bottomPanelHeight = bottomPanelPlaceables.maxByOrNull { it.height }?.height ?: 0
val bottomPanelWidth = bottomPanelPlaceables.maxByOrNull { it.width }?.width ?: 0
val middlePanelsMaxHeight = (layoutMaxHeight - topPanelHeight - bottomPanelHeight).coerceAtLeast(0)
val sidePanelInnerPadding = PaddingValues(bottom = bottomPanelHeight.toDp())
// leftPanel measuring
val leftPanelPlaceables = subcompose(FramedContentLayoutContent.LeftPanel) {
leftPanel(sidePanelInnerPadding)
}.map { it.measure(looseConstraints.copy(maxHeight = middlePanelsMaxHeight)) }
val leftPanelWidth = leftPanelPlaceables.maxByOrNull { it.width }?.width ?: 0
val leftPanelHeight = leftPanelPlaceables.maxByOrNull { it.height }?.height ?: 0
// rightPanel measuring
val rightPanelPlaceables = subcompose(FramedContentLayoutContent.RightPanel) {
rightPanel(sidePanelInnerPadding)
}.map { it.measure(looseConstraints.copy(maxHeight = middlePanelsMaxHeight)) }
val rightPanelWidth = rightPanelPlaceables.maxByOrNull { it.width }?.width ?: 0
val rightPanelHeight = rightPanelPlaceables.maxByOrNull { it.height }?.height ?: 0
val mainContentWidthMax = (layoutMaxWidth - leftPanelWidth - rightPanelWidth).coerceAtLeast(0)
// mainContent measuring
val mainContentPlaceables = subcompose(FramedContentLayoutContent.MainContent) {
val innerPadding = PaddingValues(start = leftPanelWidth.toDp(), bottom = bottomPanelHeight.toDp(), end = rightPanelWidth.toDp())
mainContent(innerPadding)
}.map { it.measure(looseConstraints.copy(maxHeight = middlePanelsMaxHeight, maxWidth = mainContentWidthMax)) }
val mainContentWidth = mainContentPlaceables.maxByOrNull { it.width }?.width ?: 0
val mainContentHeight = mainContentPlaceables.maxByOrNull { it.height }?.height ?: 0
// ==========================================================================================================+
// placing
// ==========================================================================================================+
val actualLayoutWidth = leftPanelWidth + mainContentWidth + rightPanelWidth
val maxLayoutWidth = maxOf(actualLayoutWidth, topPanelWidth, bottomPanelWidth)
val actualLayoutHeight = topPanelHeight + mainContentHeight + bottomPanelHeight
val maxLayoutHeight = maxOf(actualLayoutHeight, leftPanelHeight, rightPanelHeight)
// ==========================================================================================================+
// do some health asserts before actual placing
// ==========================================================================================================+
val actualAsserts = mutableListOf<String>()
if (bottomPanelWidth > actualLayoutWidth) {
actualAsserts.add("bottomPanel.width <= actualLayoutWidth(=leftWidth+mainWidth+rightWidth)")
}
if (topPanelWidth > actualLayoutWidth) {
actualAsserts.add("topPanel.width <= actualLayoutWidth(=leftWidth+mainWidth+rightWidth)")
}
if (leftPanelHeight > actualLayoutHeight) {
actualAsserts.add("leftPanel.height <= actualLayoutHeight(=topHeight+mainHeight+rightHeight)")
}
if (rightPanelHeight > actualLayoutHeight) {
actualAsserts.add("rightPanel.height <= actualLayoutHeight(=topHeight+mainHeight+rightHeight)")
}
if (actualAsserts.isNotEmpty()) {
printlnErr("Warning: FramedContent asserts failed for: '${actualAsserts.joinToString()}'. (Do you have a Spacer() or .weight(1f) in it?")
}
layout(maxLayoutWidth, maxLayoutHeight) {
mainContentPlaceables.forEach {
it.place(leftPanelWidth, topPanelHeight)
}
leftPanelPlaceables.forEach {
it.place( 0, topPanelHeight)
}
rightPanelPlaceables.forEach {
it.place( leftPanelWidth + mainContentWidth, topPanelHeight)
}
topPanelPlaceables.forEach {
it.place(0, 0)
}
// The bottom bar is always at the bottom of the layout
bottomPanelPlaceables.forEach {
it.place(0, topPanelHeight + mainContentHeight)
}
}
}
}
private enum class FramedContentLayoutContent { TopPanel, BottomPanel, MainContent, LeftPanel, RightPanel }