Make composable wrap content - Jetpack Compose - android

I am trying to make the ImageComposable wrap its height and width according to its content, along with the two Text composable, align to the bottom of Assemble composable. Following is the code for that:
#Composable
fun ImageComposable(url:String){
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current).data(url).apply{
placeholder(drawableResId = R.drawable.ic_broken_pic)
}.build()
)
Image(painter = painter, contentDescription = null, Modifier.padding(2.dp).border(width = 2.dp, shape = CircleShape, color = MaterialTheme.colors.onPrimary)
}
#Composable
fun Assemble(url:String){
Column (modifier = Modifier.fillMaxWidth().height(400.dp).background(MaterialTheme.colors.primary)
.padding(16.dp), verticalArrangement = Arrangement.Bottom) {
ImageComposable(url)
Text(text = "title")
Text(text = "Body")
}
}
but the ImageComposable ends up taking all the height and width of the Assemble composable and I am not able to see the two Text composables that I added in the column. So I am confused as to what is the exact problem here. I thought at least it should show the ImageComposable along with the two Text composable but it is not happening.
I am using coil image loading library here for parsing the image from url. For now in testing, I am passing url as an Empty String. Hence I am calling the composable as:
Assemble("")
I didn't find any document that would help me understand this behavior. So I wanted to know the reason to this problem and possible solutions to overcome it.

You can explicitly specify the height of each component:
fun ImageComposable(modifier: Modifier = Modifier, url: String){
//...
Image(modifier = modifier, //...
}
Column(//..
ImageComposable(modifier = Modifier.height(200.dp)//...
Text(modifier = Modifier.height(50.dp)//...
Text(modifier = Modifier.height(150.dp)//...
}
Or you can specify a fraction of the maximum height it will take up:
fun ImageComposable(modifier: Modifier = Modifier, url: String){
//...
Image(modifier = modifier, //...
}
Column(//..
ImageComposable(modifier = Modifier.fillMaxHeight(0.75f)//...
Text(modifier = Modifier.fillMaxHeight(0.1f)//...
Text(modifier = Modifier.fillMaxHeight(0.15f)//...
}
You can also try playing with the weight modifier:
fun ImageComposable(modifier: Modifier = Modifier, url: String){
//...
Image(modifier = modifier, //...
}
Column(//..
ImageComposable(modifier = Modifier.weight(1f)//...
Text(modifier = Modifier.weight(1f, fill = false)//...
Text(modifier = Modifier.weight(1f, fill = false)//...
}

It would be easier to solve your problem if there would be a sketch of what you want to achieve.
Nevertheless, I hope I can help:
It looks like the issue you are facing can be handled by Intrinsic measurements in Compose layouts.
The column measures each child individually without the dimension of your text constraining the image size. For this Intrinsics can be used.
Intrinsics lets you query children before they're actually measured.
For example, if you ask the minIntrinsicHeight of a Text with infinite width, it'll return the height of the Text as if the text was drawn in a single line.
By using IntrinsicSize.Max for the width of the Assemble composable like this:
#Composable
fun Assemble(url: String) {
Column(
modifier = Modifier
.width(IntrinsicSize.Max)
.background(MaterialTheme.colors.primary)
.padding(16.dp), verticalArrangement = Arrangement.Bottom
) {
ImageComposable(url)
Text(text = "title")
Text(text = "Body")
}
}
you can can create a layout like this:
(Please note that I am using a local drawable here)
You can now see the 2 texts and the width of the image is adjusted to the width of the texts.
Using Intrinsics to measure children in dependance to each other should help you to achieve what you wanted.
Please let me know if this layout does not meet your expectations.

Related

Using CoilImage in Jetpack Compose, how can I remove the top and bottom padding after the image is downloaded

I have a simple CoilImage implementation in my project where I download an image through an url, and then proceed to move the image state into an Image Composable. That's going smooth so far, but I'm facing a small problem where undesirable paddings are being added to the image. The image composable is inside a column with no fixed height. How can I get rid of these paddings while retaining the image dimension?
I've tried a couple of solutions such as using wrapContentHeight, putting it in a box and clipping it, using contentScale etc, but I just can't seem to get rid of the green spaces.
#Composable
fun HorizontalTextWithObject(enumerationItem: TextWithObjectPresentable) {
Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Top) {
Image(modifier = Modifier, enumerationItem)
}
Column(modifier = Modifier.weight(2f)) {
Title(modifier = Modifier, enumerationItem)
Description(enumerationItem)
}
}
}
#Composable
fun Image(modifier: Modifier,
enumerationItem:TextWithObjectPresentable) {
CoilImage(
{ request },
success = { imageState ->
imageState.imageBitmap?.let {
val width = it.width.dp
val height = ((width / 5) * 3)
Image(bitmap = it, contentDescription = "",
modifier = Modifier
.width(width)
.height(height)
.background(SDColor.green),
)
}
},
)
}
I tried ContentScale.Crop but it is not what I'm looking for as it will just fill the paddings heightwise

Horizontal Scrolling inside LazyColumn in Jetpack Compose

I'm currently trying to recreate a behavior, upon adding a new element to a LazyColumn the items start shifting to the right, in order to represent a Tree and make the elements easier to read.
The mockup in question:
Documentation
Reading through the documentation of Jetpack Compose in Lists and grids I found the following.
Keep in mind that cases where you’re nesting different direction layouts, for example, a scrollable parent Row and a child LazyColumn, are allowed:
Row(
modifier = Modifier.horizontalScroll(scrollState)
) {
LazyColumn {
// ...
}
}
My implementation
Box(Modifier.padding(start = 10.dp)) {
Row(
modifier = Modifier
.horizontalScroll(scrollState)
.border(border = BorderStroke(1.dp, Color.Black))
) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
for (i in 0..25) {
item {
OptionItem(Modifier.padding(start = (i*20).dp))
}
item {
TaskItem(Modifier.padding(start = (i*10).dp))
}
}
}
}
.
.
.
}
OptionItem represents the element with the dot at the beginning, and TaskItem the other one.
When testing the LazyColumn, it appears as if instead of having a fixed size, the size of the column starts growing just after the elements have gone outside the screen, this causes a strange effect.
As you can see in the GIF, the width of the column starts increasing after the elements no longer fit in the screen.
The Question
I want to prevent this effect from happening, so is there any way I could maintain the width of the column to the maximum all the time?
The reason that applying a simple fillMaxWidth will not work because you are telling a composable to stretch to max, but that is impossible because the view itself can stretch indefinitely since it can be horizontally scrollable. I'm not sure why do you want to prevent this behavior but perhaps maybe you want your views to have some initial width then apply the padding, while maintaining the same width. what you can do in such case is simply give your composables a specific width, or what you can do is to get the width of the box and apply them to your composables by width (i used a text in this case)
val localDensity = LocalDensity.current
var lazyRowWidthDp by remember { mutableStateOf(0.dp) }
Box(
Modifier
.padding(start = 10.dp)
.onGloballyPositioned { layoutCoordinates -> // This function will get called once the layout has been positioned
lazyRowWidthDp =
with(localDensity) { layoutCoordinates.size.width.toDp() } // with Density is required to convert to correct Dp
}
) {
val scrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(scrollState)
) {
items(25) { i ->
Text(
text = "Hello",
modifier = Modifier
.padding(start = (i * 20).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
items(25) { i ->
Text(
text = "World",
modifier = Modifier
.padding(start = (i * 10).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
}
}
Edit:
you can apply horizontal scroll to the lazy column itself and it will scroll in both directions

Android Compose: Place an element in the center and other elements above it

I need to insert a Text element Text("text center") in the center of the screen (both horizontally and vertically). Also, I want to insert the Text("text") element above Text("text center") with a blank space of 32dp:
So for example the following code is not correct because the whole block is centered and not just "text center":
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(text = "text")
Spacer(modifier = Modifier.height(32.dp))
Text(text = "text center")
}
You can use the ConstraintLayout since this type of layout would be a bit cumbersome to do properly with just the composables from the standard library.
ConstraintLayout can help place composables relative to others on the screen, and is an alternative to using multiple nested Row, Column, Box and custom layout elements.
Add this to your app.gradle file
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
and sync the project so that Gradle downloads the dependency.
As of July 2022 the stable version is 1.0.1.
Your layout needs the following constraints:
the "center text" needs to center horizontally and vertically to the parent
the "top text" needs to center horizontally to the parent
there should be a margin of 32 dp between the bottom of "top text" and the top of "center text"
In code these constraints are specified like this
ConstraintLayout(
modifier = Modifier.fillMaxSize() // copied from your example
) {
// Create references for the composables to constrain
val (topText, centerText) = createRefs()
Text(
text = "text",
modifier = Modifier.constrainAs(topText) {
// the "top text" needs to center horizontally to the parent
centerHorizontallyTo(parent)
// there should be a margin of 32 dp between the bottom
// of "top text" and the top of "center text"
bottom.linkTo(centerText.top, margin = 32.dp)
}
)
Text(
text = "text center",
modifier = Modifier.constrainAs(centerText) {
// the "center text" needs to center horizontally and
// vertically to the parent
centerHorizontallyTo(parent)
centerVerticallyTo(parent)
}
)
}
Since the official documentation on the compose version of ConstraintLayout is really limited, you can check the compose samples that are using it if you need more examples.
The official documentation for the Widget (non-compose) version of ConstraintLayout is good though, so to learn about everything that it can do, you can check https://developer.android.com/reference/androidx/constraintlayout/widget/ConstraintLayout.
Maybe some of the functionality might not be available in the compose version yet. As of July 2022 the latest alpha version is 1.1.0-alpha03
implementation "androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha03"
This can be done in many ways.
Option one using a custom Layout
#Composable
private fun TextLayout(
modifier: Modifier,
padding: Dp = 32.dp,
content: #Composable () -> Unit
) {
val density = LocalDensity.current
val paddingInPx = with(density) { padding.roundToPx() }
Layout(modifier = modifier, content = content) { measurables, constraints ->
require(measurables.size == 2)
val placeables: List<Placeable> = measurables.map { measurable: Measurable ->
measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
val topPlaceable = placeables.first()
val centerPlaceable = placeables.last()
val height = centerPlaceable.height
val totalHeight = constraints.maxHeight
val verticalCenter = (totalHeight - height)/2
layout(constraints.minWidth, totalHeight) {
centerPlaceable.placeRelative(0, verticalCenter)
topPlaceable.placeRelative(0, verticalCenter - height - paddingInPx)
}
}
}
Option 2 Using a Box and getting height of text using onTextLayout callback of Text.
Box(
modifier = Modifier
.fillMaxHeight()
.border(3.dp, Color.Red),
contentAlignment = Alignment.CenterStart
) {
val density = LocalDensity.current
var textHeight by remember { mutableStateOf(0.dp) }
Text(
"TextCenter",
fontSize = 20.sp,
onTextLayout = {
textHeight = with(density) {
it.size.height.toDp()
}
}
)
Text(
text = "Text",
modifier = Modifier.padding(bottom = 64.dp + textHeight * 2),
color = Color.Blue,
fontSize = 20.sp
)
}
But this option, it's only for demonstration, will have another recomposition since we update var textHeight by remember { mutableStateOf(0.dp) }
Option 3 is using a ConstraintLayout. For a simple layout that like this i don't think anyone counter any performance issues using ConstraintLayout but i always refrain using ConstraintLayout because it uses MultiMeasureLayout that comes with a serious warning
#Deprecated(
"This API is unsafe for UI performance at scale - using it incorrectly will lead " +
"to exponential performance issues. This API should be avoided whenever possible."
)
fun MultiMeasureLayout(
modifier: Modifier = Modifier,
content: #Composable #UiComposable () -> Unit,
measurePolicy: MeasurePolicy
) {
}

Accompanist FlowRow : is it possible to scroll down automatically in order to show an element?

In my Jetpack Compose project, one of my components uses a FlowRow from Accompanist.
But I don't know how to make the FlowRow scroll to a given "node".
Here the relevant code from my #Composable:
sealed class MovesNavigatorElement(open val text: String)
data class MoveNumber(override val text: String) : MovesNavigatorElement(text)
data class HalfMoveSAN(override val text: String) : MovesNavigatorElement(text)
#Composable
fun MovesNavigator(modifier: Modifier = Modifier, elements: Array<MovesNavigatorElement>, mustBeVisibleByDefaultElementIndex: Int) {
val vertScrollState = rememberScrollState()
FlowRow(
modifier = modifier
.background(color = Color.Yellow.copy(alpha = 0.3f))
.verticalScroll(vertScrollState),
mainAxisSpacing = 10.dp,
crossAxisSpacing = 15.dp,
) {
elements.map {
Text(text = it.text, fontSize = 34.sp, color = Color.Blue, style= MaterialTheme.typography.body1)
}
}
}
Where you can see that I declare the "nodes" of the FlowRow as a list : the parameter elements. Also I'm using a ScrollState in the local variable vertScrollState.
But, let's say that I want to make it scroll to elements[30] : how should I do that ? Given that mustBeVisibleByDefaultElementIndex is the index of the element that must be visible by default. I mean, when composition occurs. But the user can change the position later of course.
In other words :
At composition : the element whose index is given is made visible
Then, before any other composition occurs of course, the user can scroll it with the scrollbar.
You just need to use the method parameter,
val state = rememberScrollState(initial = mustBeVisibleByDefault)
Better press Ctrl + P to see all possible combinations before going to even the web.
I kept this in case if anyone finds it helpful bizarrely:-
The ScrollState exposes scrollTo and animateScrollTo methods. You can easily use them to achieve the desired result. Refer to the docs
Here how I manage to solve (partially) my issue :
use a plain ScrollState instead of rememberScrollState() : because even if I set scroll to a fixed value at recomposition, the user still will be able to move it. So, no need to "cache" the scroll value
use sp to px conversion for the scroll amount which is expected a value in pixels, and an hard-coded division amount of the given index (that's why it is only partially solved)
Which led me to the following :
sealed class MovesNavigatorElement(open val text: String)
data class MoveNumber(override val text: String) : MovesNavigatorElement(text)
data class HalfMoveSAN(override val text: String) : MovesNavigatorElement(text)
#Composable
fun MovesNavigator(modifier: Modifier = Modifier, elements: Array<MovesNavigatorElement>, mustBeVisibleByDefaultElementIndex: Int = 0) {
val lineHeightPixels = with(LocalDensity.current) {34.sp.toPx()}
val scrollAmount = ((mustBeVisibleByDefaultElementIndex / 6) * lineHeightPixels).toInt()
val vertScrollState = ScrollState(scrollAmount)
FlowRow(
modifier = modifier
.background(color = Color.Yellow.copy(alpha = 0.3f))
.verticalScroll(vertScrollState),
mainAxisSpacing = 8.dp,
) {
elements.map {
Text(text = it.text, fontSize = 34.sp, color = Color.Blue, style= MaterialTheme.typography.body1)
}
}
}
But still have to test on several devices.

Jetpack Compose - HorizontalPager item spacing/padding for items with max width

Using Jetpack Compose and the accompanist pager, I'm trying to create a HorizontalPager where:
Edges of items to the left and right of current item are shown
There is a max width to the Pager items
As an example, I wrote the following code (for simplicities sake, I made Text items, but in reality, I'm actually making more complex Card items):
#Composable
fun MyText(modifier: Modifier) {
Text(
text = LOREM_IPSUM_TEXT,
modifier = modifier
.wrapContentHeight()
.border(BorderStroke(1.dp, Color.Red))
)
}
#ExperimentalPagerApi
#Composable
fun MyPager(pagerItem: #Composable () -> Unit = {}) {
Scaffold {
Column(
modifier = Modifier
.fillMaxSize()
// In case items in the VP are taller than the screen -> scrollable
.verticalScroll(rememberScrollState())
) {
HorizontalPager(
contentPadding = PaddingValues(32.dp),
itemSpacing = 16.dp,
count = 3,
) {
pagerItem()
}
}
}
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_200dpWidth() {
MyPager { MyText(modifier = Modifier.widthIn(max = 200.dp)) }
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_500dpWidth() {
MyPager { MyText(modifier = Modifier.widthIn(max = 500.dp)) }
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_FillMaxWidth() {
MyPager { MyText(modifier = Modifier.fillMaxWidth()) }
}
The issue I'm having is that when I make the item have a max width that seems to be smaller than the screen width (see MyPager_200dpWidth), I no longer see the items on the side anymore. On the other hand, using items with larger max widths (See MyPager_500dpWidth) or fillMaxWidth (See MyPager_FillMaxWidth) allows me to see the items on the side.
It seems weird to me that items with the smaller max width are actually taking up more horizontal space than the items with the larger max width. This can be shown in the Preview section...
See how the images on the left (MyPager_500dpWidth) and middle (MyPager_FillMaxWidth) show the next item, while the image on the right (MyPager_200dpWidth`) takes up the whole screen? Here's another comparison when hovering my mouse over the items to see the skeleton boxes...
Just wondering if someone could help me figure out how I can solve this use case. Is it possible that there's a bug in Compose?
The page size is controlled by the HorizontalPager.contentPadding parameter.
Applying widthIn(max = 200.dp) to your text only reduces the size of your element inside the page, while the page size remains unchanged.
Applying more padding should solve your problem, for example, contentPadding = PaddingValues(100.dp) looks like this:
You can fix this by adding your contentPadding like this
val horizontalPadding = 16.dp
val itemWidth = 340.dp
val screenWidth = LocalConfiguration.current.screenWidthDp
val contentPadding = PaddingValues(start = horizontalPadding, end = (screenWidth - itemWidth + horizontalPadding).dp)

Categories

Resources