Avoid one frame delay in calculating child size - android-jetpack-compose

I have to create a reusable component that tries to achieve this goal: I have a column that can have content that's larger than the screen height. On the bottom of the screen we have panel with gradient background that can contain button or something else (it's basically a slot in the component). This bottom panel have to be always visible on the screen, and in case of the column being bigger than screen - bottom panel have to be on the top of this column. Gradient background does a nice UX effect so user knows what is going on. It looks like that:
I have that solved, but here's the challenge. The column content have to be scrollable to be on top of the bottom panel when scrolled to the end. Current solution I have is to add a spacer on the bottom of this column. This spacer have the calculated height of the bottom parent. And here's the issue - right now we have calculation done in onSizeChanged which basically results in additional frame needed for the spacer to have correct size.
We did not observe any negative impact of that performance or UX wise. The spacer height calculation never does anything that user can see, but I still want to solve that properly.
AFAIK this can be done using custom Layout, but that seems a little bit excessive for what I want to achieve. Is there another way to do this properly?
Current solution:
#Composable
fun FloatingPanelColumn(
modifier: Modifier = Modifier,
contentModifier: Modifier = Modifier,
contentHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
bottomPanelContent: #Composable ColumnScope.() -> Unit,
content: #Composable ColumnScope.() -> Unit
) {
val scrollState = rememberScrollState()
var contentSize by remember {
mutableStateOf(1)
}
Box(modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState)
.then(contentModifier),
horizontalAlignment = contentHorizontalAlignment,
) {
content()
val contentSizeInDp = with(LocalDensity.current) { contentSize.toDp() }
Spacer(modifier = Modifier.height(contentSizeInDp))
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.onSizeChanged {
contentSize = it.height
}
.wrapContentHeight()
.align(Alignment.BottomStart)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0x00FAFCFF),
Color(0xFFF6F9FB),
)
)
),
content = bottomPanelContent
)
}
}

The best way to depend on an other view size during layout is using SubcomposeLayout:
SubcomposeLayout { constraints ->
// subcompose the view you need to measure first
val bottomPanel = subcompose("bottomPanel") {
Column(
// ...
)
}[0].measure(constraints)
// use calculated value in next views layout, like bottomPanel.height
val mainList = subcompose("mainList") {
LazyColumn(
contentPadding = PaddingValues(bottom = bottomPanel.height.toDp())
) {
// ...
}
}[0].measure(constraints)
layout(mainList.width, mainList.height) {
mainList.place(0, 0)
bottomPanel.place(
(mainList.width - bottomPanel.width) / 2,
mainList.height - bottomPanel.height
)
}
}

Related

Jetpack compose: how to set intrinsic height as minimum, but expand to fill space?

I'm trying to set it so that a TextField refuses to shrink past its intrinsic height (defined by minLines), but also grow to fill max height:
// context: this is inside of a Column that has fillMaxHeight on it
OutlinedTextField(
// unrelated fields
minLines = 26,
modifier = Modifier
//.height(IntrinsicSize.Min)
.fillMaxWidth()
.fillMaxHeight()
.weight(1f)
.height(IntrinsicSize.Max)
)
However, everything I've tried (setting fillMaxHeight, weight(1f), using requiredHeight instead of height, etc) has issues. It seems like it's mutually exclusive: I can either set a minimum height based on intrinsic height or I can grow to fill the height (using weight seems to work for that, but not fillMaxHeight for some reason.
What is the proper way to have IntrinsicSize be the minimum, but grow to fill remaining space?
If i understand this question correctly you want your TextField height to be one line tall then increases as user enters text. You can selectively change modifier using Modifier.then()
Edit
As of jetpack compose 1.4.0-alpha02 minLines param is added to OutlineTextField and other text composables
#Composable
private fun Sample() {
var text by remember {
mutableStateOf("Hello World")
}
var focused by remember {
mutableStateOf(false)
}
Column(modifier = Modifier.fillMaxHeight()) {
OutlinedTextField(
value = text,
onValueChange = {
text = it
},
minLines = 26,
// unrelated fields
modifier = Modifier
.fillMaxWidth()
.then(
if (focused) {
Modifier.wrapContentHeight()
} else Modifier.fillMaxHeight()
)
.onFocusChanged {
focused = it.isFocused
}
)
}
}

Why the Row in Card out of the scope of Card with fillMaxWidth()

My code:
#ExperimentalMaterial3Api
#Composable
fun Settings() {
Card(modifier = Modifier
.fillMaxWidth()
) {
SettingItem(itemTitle = "Test Title", itemDescription = "Description")
}
}
#ExperimentalMaterial3Api
#Composable
fun SettingItem(itemTitle: String, itemDescription: String) {
Row(modifier = Modifier.padding(12.dp).fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(text = itemTitle, color = MaterialTheme.colorScheme.primary)
Text(text = itemDescription, color = MaterialTheme.colorScheme.secondary)
}
Spacer(modifier = Modifier.size(width = 12.dp, height = 4.dp))
Switch(checked = false, onCheckedChange = {})
}
}
I want to put a Switch at the end of a SettingItem, so I set fillMaxWidth(). But the Switch shows out of the layout of Card.
Image
Change the Column modifier to weight.
From,
modifier = Modifier.fillMaxWidth()
To
modifier = Modifier.weight(1F)
The fillMaxWidth modifier is used to make the Column as wide as possible. Whereas here the requirement is to fill as wide as possible leaving space for the Spacer and Switch. That is achieved by using the weight modifier.
Weight
The parent will divide the horizontal space remaining after measuring unweighted child elements and distribute it according to this weight.
FillMaxWidth
Have the content fill (possibly only partially) the Constraints.maxWidth of the incoming measurement constraints, by setting the minimum width and the maximum width to be equal to the maximum width multiplied by fraction.
Note that, by default, the fraction is 1, so the modifier will make the content fill the whole available width.
Reference
weight
fillMaxWidth

android compose lazy column separate by weight

Is it possible to do weights in Jetpack Compose with lazy column?
I'd like to set it menu item is weighted as 1/n (n = number of menus) of a layout, and the other takes up the remaining 1/n, also.
I want to list it at the same height as the number of menus.
MenuList
#Composable
fun MenuList(
loading: Boolean,
menus: List<Menu>,
onNavigateToMenuDetailScreen: (String) -> Unit
) {
Box(modifier = Modifier
.background(color = MaterialTheme.colors.surface)
.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
if (loading && menus.isEmpty()) {
LoadingShimmer(imageHeight = 800.dp)
}
else if (menus.isEmpty()) {
NothingHere()
}
else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1F)
) {
itemsIndexed(
items = menus
) { index, menu ->
MenuCard(
menu = menu,
onClick = {
}
)
}
}
}
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = Modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column {
Text(
text = menu.name,
fontSize = 30.sp,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.wrapContentHeight(Alignment.CenterVertically),
style = MaterialTheme.typography.h3
)
}
}
}
In summary, MenuCards are created as many as the number of menu on the MenuList screen, and I hope the height of each MenuCard will be 1/n.
(n = number of menu)
Like, when number of menu is 8,
Just same height to each menu.
Replace your LazyColumn code with BoxWithConstraints and regular Column.
BoxWithConstraints for getting the minHeight or in this case you can say screen height.
Change to something like the below.
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val height = minHeight/menus.size
Column(
modifier = Modifier
.fillMaxSize()
) {
menus.forEachIndexed { index, menu ->
MenuCard(
modifier = Modifier.height(height),
menu = menu,
onClick = {
}
)
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column(
modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = menu.name,
fontSize = 30.sp,
style = MaterialTheme.typography.h3
)
}
}
}
You will get this
3 items and 6 items
If all you want is a way to divide the screen height in equal proportions to all the n items, you can simply go for a column.
val items = ... //I assume you have access to this
val screenHeight = LocalConfiguration.current.screenHeightDp
Column{
items.forEach{
MenuCard(modifier = Modifier.height(1f / items.size * screenHeight))
}
}
Column and LazyColumn are fundamentally different. LazyColumn's sole purpose is to deal with large datasets by loading and caching only a small window off that set. Now, if there is nothing to be scrolled, LazyColumn is useless, in fact, it is worse.
EDIT:
You could use BoxWithConstraints to get the screen dimensions as well, but it, in this context, would be more of a workaround than a solution. It adds an extra Composable and decreases the readability of the code too, so for getting the screen dimensions, always use the API that was specifically built for the job.

How to drag something behind another view

I'm trying to write a calendar like custom view as described in this blog post, which is also kind of like an Excel spreadsheet. Headers on the top and column counts on the left. Those are stickied to the top and left
https://danielrampelt.com/blog/jetpack-compose-custom-schedule-layout-part-1/
Unlike this blog post, which adds a horizontal / vertical scroll modifier on the content which keeps the headers in place works, I need to be able to drag my content in all directions.
It looked like I needed to use a pointInput with a detectDragGestures, but when I use that, it drags like i want, the headers are pinned to the top and move left/right and the column counts are pinned to the left and move up and down
But the schedule content scrolls over the columns/headers as i drag them upwards or left.
I need the dragged content to go behind the header/column indicators.
My layout is like this
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Column(
modifier = modifier.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX = (offsetX + dragAmount.x).coerceIn((-1 * unitWidth.toPx() * unitList.count()), 0f)
offsetY = (offsetY + dragAmount.y).coerceIn(-1 * hourHeight.toPx() * 24, 0f)
}
}
) {
Header(modifier = Modifier.offset { IntOffset(offsetX.roundToInt(), 0) } )
Row(modifier = Modifier.weight(1f)) {
SideBar( modifier = Modifier.offset { IntOffset(0, offsetY.roundToInt())})
Schedule( modifier = Modifier.offset { IntOffset(offsetX.roundToInt() + unitWidth.roundToPx(), offsetY.roundToInt()) }.weight(1f)) } )
)
}
How do I enable dragging in any direction, and not overlap the other views? I want the content to be hidden / vanish behind the headers as it goes into them.
Just set the z-index of the headers and stuff to something like 5 and that of the draggable to a lower value. It will automatically be clipped upon appropriate position attainment

Jetpack Compose - Row Clipping Children When Width Increases

Here on the right , I have a list of items in a composable , Every item is inside a row , All the items are inside a column
All the children of the are getting clipped to fit the screen which I don't want , I want these items to render completely even if outside of screen since I have a zoomable container above them
As you can see how text in the text field is all in one line vertically rather than expanding the width , This is the problem
Code :
Row(
modifier = modifier.zIndex(3f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
SimpleNodesList(
modifier = Modifier.padding(8.dp),
parentNode = state.center,
nodes = state.center.left,
renderRight = false,
)
SimpleNode(node = state.center, parentNode = null)
SimpleNodesList(
modifier = Modifier.padding(8.dp),
parentNode = state.center,
nodes = state.center.right,
renderLeft = false
)
}
Simple Nodes List is a column of rows , I have one column on left and one on the right , If the width of the left column increases , right row get's clipped by the screen
Using this modifier does the job for the row , In my case I also needed this layout modifier , wrapContentSize(unbounded = true) was working but children weren't clickable for some reason outside the bounds of the zoom container !
I also had to create a modifier zoomable rather than use a zoomable box , so the zoomable touch events would be dispatched on the this composable rather than the parent !
modifier = Modifier
.layout { measurable, constraints ->
val r =
measurable.measure(constraints = constraints.copy(maxWidth = Constraints.Infinity))
layout(r.measuredWidth, r.measuredHeight, placementBlock = {
r.placeWithLayer(0, 0, 0f) {
}
})
}
.wrapContentSize(unbounded = true)
If you are using hard-coded width for the text, applying Modifier.wrapContentSize() on every container might do the job
Use SimpleFlowRow in place of Row. It will fix the clipping issue.

Resources