I have a Column with some rows, and I want to align the last row at the botton, but this row is never located at the bottom of the screen, it stays right after the previous row:
Column {
// RED BOX
Row(
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.padding(vertical = 15.dp, horizontal = 30.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(id = R.string.app_name),
style = TextStyle(fontSize = 40.sp),
color = Color.White
)
Text(
text = stringResource(id = R.string.app_description),
style = TextStyle(fontSize = 13.sp),
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(15.dp)
)
// GREEN BOX
val currentRoute = currentRoute(navController)
items.forEach { item ->
DrawerItem(item = item, selected = currentRoute == item.route) {
navController.navigate(item.route) {
launchSingleTop = true
}
scope.launch {
scaffoldState.drawerState.close()
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 30.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.Center
) {
Text(
text = BuildConfig.VERSION_NAME,
style = TextStyle(fontSize = 11.sp),
color = Color.Black,
)
}
}
I want to get the same as I show in the picture. I want to have the first row (red), then the second row (green) and then a third row that fits at the bottom of the screen (blue)
You can do it in many ways.
You can use a Column with verticalArrangement = Arrangement.SpaceBetween assigning a weight(1f,false) to the last row:
Column(
Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween) {
//All elements
Column {
// RED BOX
//...
Spacer(
modifier = Modifier
.fillMaxWidth()
.background(Green)
.height(15.dp)
)
//... Green box
}
//LAST ROW
Row(
modifier = Modifier
.weight(1f, false)
) {
//...
}
}
You can use a Spacer(modifier.weight(1f)) between GreenBox and Blue Box to create space between them or you can create your custom column with Layout function and set y position of last Placeable as height of Composable - height of last Composble
Column(modifier = Modifier
.fillMaxHeight()
.background(Color.LightGray)) {
Text(
"First Text",
modifier = Modifier
.background(Color(0xffF44336)),
color = Color.White
)
Text(
"Second Text",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Third Text",
modifier = Modifier
.background(Color(0xff2196F3)),
color = Color.White
)
}
Result:
Custom Layout
#Composable
private fun CustomColumn(
modifier: Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val looseConstraints = constraints.copy(
minWidth = 0,
maxWidth = constraints.maxWidth,
minHeight = 0,
maxHeight = constraints.maxHeight
)
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(looseConstraints)
}
// Track the y co-ord we have placed children up to
var yPosition = 0
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children in the parent layout
placeables.forEachIndexed { index, placeable ->
println("Placeable width: ${placeable.width}, measuredWidth: ${placeable.measuredWidth}")
// Position item on the screen
if (index == placeables.size - 1) {
placeable.placeRelative(x = 0, y = constraints.maxHeight - placeable.height)
} else {
placeable.placeRelative(x = 0, y = yPosition)
}
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
Usage
CustomColumn(
modifier = Modifier
.fillMaxHeight()
.background(Color.LightGray)
) {
Text(
"First Text",
modifier = Modifier
.background(Color(0xffF44336)),
color = Color.White
)
Text(
"Second Text",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Third Text",
modifier = Modifier
.background(Color(0xff2196F3)),
color = Color.White
)
}
Result:
In this example with Layout you should consider how you measure your measureables with Constraints and your total width and height. It requires a little bit practice but you get more unique designs and with less work(more optimised) composables than ready ones. Here i set layout as maxWidth so no matter which width you assign it takes whole width. It's for demonstration you can set max width or height in layout based on your needs.
Related
I am building a basic layout which diplays a horizontally scrollable row with cards. The cards are a column component with a couple dividers in the middle:
#Composable
fun TestCard(
name: String
) {
Column(
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(12.dp),
horizontalAlignment = Alignment.Start
) {
Text(text = name, style = Typography.h4.bold.black)
Spacer(modifier = Modifier.height(8.dp))
Divider(color = Color.Black) //DOES NOT APPEAR IN ROW
Spacer(modifier = Modifier.height(8.dp))
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black) //DOES NOT APPEAR IN ROW
Spacer(modifier = Modifier.height(8.dp))
Text(text = "Other text", style = Typography.h4.bold.black)
}
}
I also have a scrollable row that contains a list of these cards:
#Composable
fun TestComponent() {
val scrollState = rememberScrollState()
val names = listOf("Martha", "Erik","Steve","Roy","Pete")
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary),
horizontalAlignment = Alignment.Start
) {
Row(Modifier.horizontalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
names.forEach { name ->
TestCard(name)
}
}
}
}
When I display the card on its own, I can see both dividers. When I display the row containing cards the dividers don't appear, because their width is set to 0 (I can verify with LayoutInspector the dividers are there). Visual representation:
I know I can create a state variable for width that would be updated inside the Modifier.onSizeChanged but I am sure there must be a reason for this to happen and a better solution.
The Divider has inside a fillMaxWidth() modifier, but as you check in the doc:
If the incoming maximum width is Constraints.Infinity this modifier will have no effect.
It happens because of the horizontalScroll.
You have to specify the width for the Dividers and the Spacers.
A workaround is to apply width(IntrinsicSize.Max) to the Column in the TestCard. In this way the Constraints have a Constraints.fixedWidth(width):
#Composable
fun TestCard(
name: String
) {
Column(
modifier = Modifier
.width(IntrinsicSize.Max)
.background(androidx.compose.material.MaterialTheme.colors.surface)
.padding(12.dp),
horizontalAlignment = Alignment.Start
) {
Text(text = name,)
Spacer(modifier = Modifier.height(8.dp).fillMaxWidth())
Divider(color = Color.Black, modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp).fillMaxWidth())
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black)
Spacer(modifier = Modifier.height(8.dp).fillMaxWidth())
Text(text = "Other text" )
}
}
Is there any way how I can show a little description above specific icon in Jetpack Compose like in this picture?
It's called speech or tooltip bubble. You can create this or any shape using GenericShape or adding RoundedRect.
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
var showToolTip by remember {
mutableStateOf(false)
}
Spacer(modifier = Modifier.height(100.dp))
val triangleShape = remember {
GenericShape { size: Size, layoutDirection: LayoutDirection ->
val width = size.width
val height = size.height
lineTo(width / 2, height)
lineTo(width, 0f)
lineTo(0f, 0f)
}
}
Box {
if (showToolTip) {
Column(modifier = Modifier.offset(y = (-48).dp)) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.shadow(2.dp)
.background(Color(0xff26A69A))
.padding(8.dp),
) {
Text("Hello World", color = Color.White)
}
Box(
modifier = Modifier
.offset(x = 15.dp)
.clip(triangleShape)
.width(20.dp)
.height(16.dp)
.background(Color(0xff26A69A))
)
}
}
IconButton(
onClick = { showToolTip = true }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "null",
Modifier
.background(Color.Red, CircleShape)
.padding(4.dp)
)
}
}
}
If you need shadow or border that must be a single shape you need to build it with GenericShape. You can check my answer out and library i built.
The sample below is simplified version of library, with no Modifier.layout which is essential for setting space reserved for arrow and setting padding correctly instead of creating another Box with Padding
Result
fun getBubbleShape(
density: Density,
cornerRadius: Dp,
arrowWidth: Dp,
arrowHeight: Dp,
arrowOffset: Dp
): GenericShape {
val cornerRadiusPx: Float
val arrowWidthPx: Float
val arrowHeightPx: Float
val arrowOffsetPx: Float
with(density) {
cornerRadiusPx = cornerRadius.toPx()
arrowWidthPx = arrowWidth.toPx()
arrowHeightPx = arrowHeight.toPx()
arrowOffsetPx = arrowOffset.toPx()
}
return GenericShape { size: Size, layoutDirection: LayoutDirection ->
val rectBottom = size.height - arrowHeightPx
this.addRoundRect(
RoundRect(
rect = Rect(
offset = Offset.Zero,
size = Size(size.width, rectBottom)
),
cornerRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)
)
)
moveTo(arrowOffsetPx, rectBottom)
lineTo(arrowOffsetPx + arrowWidthPx / 2, size.height)
lineTo(arrowOffsetPx + arrowWidthPx, rectBottom)
}
}
Then create a Bubble Composable, i set static values but you can set these as parameters
#Composable
private fun Bubble(
modifier: Modifier = Modifier,
text: String
) {
val density = LocalDensity.current
val arrowHeight = 16.dp
val bubbleShape = remember {
getBubbleShape(
density = density,
cornerRadius = 12.dp,
arrowWidth = 20.dp,
arrowHeight = arrowHeight,
arrowOffset = 30.dp
)
}
Box(
modifier = modifier
.clip(bubbleShape)
.shadow(2.dp)
.background(Color(0xff26A69A))
.padding(bottom = arrowHeight),
contentAlignment = Alignment.Center
) {
Box(modifier = Modifier.padding(8.dp)) {
Text(
text = text,
color = Color.White,
fontSize = 20.sp
)
}
}
}
You can use it as in this sample. You need to change offset of Bubble to match position of ImageButton
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
var showToolTip by remember {
mutableStateOf(false)
}
Spacer(modifier = Modifier.height(100.dp))
Box {
if (showToolTip) {
Bubble(
modifier = Modifier.offset(x = (-15).dp, (-52).dp),
text = "Hello World"
)
}
IconButton(
onClick = { showToolTip = true }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "null",
Modifier
.background(Color.Red, CircleShape)
.padding(4.dp)
)
}
}
}
You can use a Box. The children of the Box layout will be stacked over each other.
Box{
Text(text = "Text Above Icon", modifier = text alignment)
Icon(... , modifier = icon alignment)
}
Text can be added above an icon in Jetpack Compose by using a combination of the Row and Column composables. The Row composable lays out its children in a single row while the Column composable lays out its children in a single column. To add text above the icon, the Row composable should be used first, followed by the Column composable. This will allow the text to be placed on the top of the icon. For example, the following code will add text above an icon:
Row {
Text(text = "Text Above Icon")
Column {
Icon(... )
}
}
I am making a "ToggleGroup" with Jetpack-Compose, using essentially a Row into which I print Text. I manage to make it work if I tune the width manually (.width(70.dp) in the code below), but I would like it to automatically do that.
I essentially want this:
But without manually adding .width(70.dp), I get this:
My current (tuned) code is the following:
Row(
horizontalArrangement = Arrangement.End,
) {
options.forEach { option ->
val isSelected = option == selectedOption
val textColor = if (isSelected) Color.White else MaterialTheme.colors.primary
val backgroundColor = if (isSelected) Color.Gray else Color.White
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(70.dp)
.background(backgroundColor)
.clickable { onSelectionChanged(option) }
) {
Text(
text = option,
color = textColor,
modifier = Modifier.padding(14.dp),
)
}
}
}
It feels similar to this question, but somehow it's different because I use Row and the question uses Column (or at least I did not manage to use Intrinsics correctly).
How could I do that?
Changes required.
1. On the parent Row, use Modifier.width(IntrinsicSize.Min)
(min|max)IntrinsicWidth: Given this height, what's the minimum/maximum width you can paint your content properly?
Source - Docs
2. Use Modifier.weight(1F) on all children.
Size the element's width proportional to its weight relative to other weighted sibling elements in the Row.
The parent will divide the horizontal space remaining after measuring unweighted child elements and distribute it according to this weight.
Source - Docs
3. Use Modifier.width(IntrinsicSize.Max) on all children.
This ensures the Text inside the children composables are not wrapped.
(You can verify this by removing the modifier and adding long text)
Screenshot
Sample code
#Composable
fun AutoWidthRow() {
val items = listOf("Item 1", "Item 2", "Item 300")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.width(IntrinsicSize.Min),
) {
items.forEach { option ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(IntrinsicSize.Max) // Removing this will wrap the text
.weight(1F)
.background(Color.Black)
) {
Text(
text = option,
color = Color.White,
modifier = Modifier
.padding(14.dp),
)
}
}
}
}
You should create a custom Layout
#Composable
fun EqualSizeTiles(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
layoutTiles(
measurables,
constraints
)
}
}
private fun MeasureScope.layoutTiles(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
val tileHeight = constraints.maxHeight
val tileWidths = measurables.map { measurable ->
measurable.maxIntrinsicWidth(tileHeight)
}
val tileWidth = tileWidths.maxOrNull() ?: 0
val tileConstraints = Constraints(
minWidth = tileWidth,
minHeight = 0,
maxWidth = tileWidth,
maxHeight = constraints.maxHeight,
)
val placeables = measurables.map { measurable ->
measurable.measure(tileConstraints)
}
val width = (placeables.size * tileWidth).coerceAtMost(constraints.maxWidth)
return layout(width = width, height = tileHeight) {
placeables.forEachIndexed { index, placeable ->
placeable.place(tileWidth * index, 0)
}
}
}
#Preview(showBackground = true, widthDp = 512)
#Composable
private fun EqualSizeTilesPreview() {
WeatherSampleTheme {
Surface(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
EqualSizeTiles(
modifier = Modifier
.height(64.dp)
.background(color = Color.Green)
.padding(all = 8.dp)
) {
Text(
text = "Left",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Red)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Center",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Yellow)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Right element",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Blue)
.padding(all = 8.dp)
.fillMaxHeight(),
)
}
}
}
}
Why the width of red circle equal to with of gray area?
Here are my code and result, Hope someone answers.
#Preview
#Composable
fun testContent() {
Scaffold { paddingValues ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth(1f)
.aspectRatio(0.8f)
.clip(RoundedCornerShape(size = 6.dp))
.background(Color.Gray)
.fillMaxWidth(0.8f)
.aspectRatio(ratio = 1f)
.clip(CircleShape)
.background(Color.Red),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "I am text",
color = Color.White,
)
Text(
text = "I am text too",
color = Color.White,
)
}
}
}
}
result is here: https://i.stack.imgur.com/qUUNM.png
Because in Column Composable (which draws/clips red circle) you have a parameter Modifier.fillMaxWidth(1f), thus it takes the maximum width of the parent
1f is is a fraction parameter and if you make it less it will take less width
I am making a layout where a column contains up to 5 rows. Each row has three columns and I would like to have the width of the first column of each row be completely equal.
The first column in each row always takes up as much size as the text it contains, the size of the column doesn't scale to match the size of the biggest column in the list of rows.
Each row item:
fun GetRowItem(firstColumnText: String, secondColumnText: String, thirdColumnText: String) {
Row(
modifier = Modifier.padding(start = 4.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.padding(end = 4.dp),
) {
Text(
text = firstColumnText
)
}
Column(
modifier = Modifier.padding(end = 4.dp),
) {
Text(
text = secondColumnText
)
}
Column(
modifier = Modifier.padding(end = 4.dp),
) {
Text(
text = thirdColumnText
)
}
}
}
Then I have a parent that arranges the rows something like this:
Column(modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 8.dp) {
GetRowItem(firstColumnText = "short", secondColumnText = "something", thirdColumnText = "something")
GetRowItem(firstColumnText = "Realy long ee", secondColumnText = "something", thirdColumnText = "something")
GetRowItem(firstColumnText = "1", secondColumnText = "something", thirdColumnText = "something")
}
The output of this is not what I am looking for and I am struggling to see how to align the columns in each row item. It looks like InstrisicSize might be an option here, but I'm not sure how to get the max size of an arbitrary column in the list of rows and apply it to each column. The image below shows what I am getting versus what I am expecting. I only want to align the first columns, the rest don't matter.
An easy way to achieve this would be to give each of the columns the same weight.
Column(modifier = Modifier.weight(1f)){
// text
}
I have found a way to solve your issue
Get Row Item
#Composable
fun GetRowItem(firstColumnText: String, secondColumnText: String, thirdColumnText: String, width: State<Int>, widthCallback: (Int) -> Unit) {
val widthInDp = with(LocalDensity.current) {
width.value.toDp()
}
Row(
modifier = Modifier.padding(start = 4.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.padding(end = 4.dp),
) {
if (widthInDp == 0.dp) {
Text(
text = firstColumnText,
modifier = Modifier.onGloballyPositioned {
widthCallback(it.size.width)
}
)
}else {
Text(
text = firstColumnText,
modifier = Modifier.width(width = widthInDp)
)
}
}
Column(
modifier = Modifier.padding(end = 4.dp),
) {
Text(
text = secondColumnText
)
}
Column(
modifier = Modifier.padding(end = 4.dp),
) {
Text(
text = thirdColumnText
)
}
}
}
Parent code that holds the row
Surface(color = MaterialTheme.colors.background) {
val width = remember {
mutableStateOf(0)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 8.dp, end = 8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
GetRowItem(firstColumnText = "short", secondColumnText = "something", thirdColumnText = "something", width = width) {
if (it > width.value) {
width.value = it
}
}
GetRowItem(firstColumnText = "Realy long ee", secondColumnText = "something", thirdColumnText = "something", width = width) {
if (it > width.value) {
width.value = it
}
}
GetRowItem(firstColumnText = "1", secondColumnText = "something", thirdColumnText = "something", width = width) {
if (it > width.value) {
width.value = it
}
}
}
}
}
}
}
I'have added surface and modified column to fillMaxSize() so you can test this
maybe it could help
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
}
if you want the center column to be wider than the left and right column you can set the weight of the center column 2 and left and right 1, or you can set the width of columns for example to 100 dp or 200 dp, and set horizontalArrangement of your row to Arrangement.SpaceBetween to have some margin around the columns