Jetpack Compose find parents width/length - android

I want to explicitely retrieve the value of the fillMaxSize().
Suppose i have:
Box(Modifier
.fillMaxSize()
.background(Color.Yellow))
{
var size = ?
Box(Modifier
.size(someSize)
.background(Color.Blue))
{Text("Test")}
I want to change the size of my second Box multiple times (will probably stem from some viewmodel) and then reset it to maxSize.
How can i do that, I don't know any 'getMaxSize()'-method?

If you really need the raw size value, you can use the following code:
var size by remember { mutableStateOf(IntSize.Zero) }
Box(Modifier
.fillMaxSize()
.background(Color.Yellow)
.onSizeChanged {
size = it
}
) {
Box(
Modifier
.then(
with(LocalDensity.current) {
Modifier.size(
width = size.width.toDp(),
height = size.height.toDp(),
)
}
)
.background(Color.Blue)
) { Text("Test") }
}
But note, that is't not optimal in terms of performance: this view gets rendered two times. First time second box gets size zero, then the onSizeChanged block gets called, and then view gets rendered for the second time.
Be especially careful if using remember in top level views, because changing state will trigger full view stack re-render. Usually you want split your screen into views with states, so changing one view state only will re-render this view.
Also you can use BoxWithConstraints where you can get maxWidth/maxHeight inside the BoxWithConstraintsScope: it's much less code and a little better on performance.
BoxWithConstraints(
Modifier
.fillMaxSize()
.background(Color.Yellow)
) {
Box(
Modifier
.size(
width = maxWidth,
height = maxHeight,
)
.background(Color.Blue)
) { Text("Test") }
}
But usually if you wanna indicate size dependencies, using modifiers without direct knowing the size should be enough. It's more "Compose" way of writing code and more optimized one.
So if you wanna you second box be same size as the first one, just use .fillMaxSize() on it too. If you wanna it to be some part of the parent, you can add fraction param. To make second box size be half size of the first one, you do:
Box(
Modifier
.fillMaxSize(fraction = 0.5f)
) { Text("Test") }
If you wanna different parts for width/height:
Box(
Modifier
.fillMaxWidth(fraction = 0.3f)
.fillMaxHeight(fraction = 0.7f)
) { Text("Test") }

In your first Box you can use the onGloballyPositioned modifier to get the size.
It is called with the final LayoutCoordinates of the Layout when the global position of the content may have changed.
Then use coordinates.size to get the size of the first Box.
var size by remember { mutableStateOf(Size.Zero)}
Box(
Modifier
.fillMaxSize()
.background(Color.Yellow)
.onGloballyPositioned { coordinates ->
size = coordinates.size.toSize()
}
Box(){ /*....*/ }
)

Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var parentSize by remember {
mutableStateOf(Size.Zero)
}
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.background(Color.Green)
) {
Box(
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
.background(Color.Red)
.onGloballyPositioned {
//here u can access the parent layout coordinate size
parentSize = it.parentLayoutCoordinates?.size?.toSize()?: Size.Zero
}
) {
Column(Modifier.fillMaxSize()) {
Text(text = "parent size = $parentSize")
}
}
}
}

Related

Jetpack Compose - Setting dimensions for Accompanist placeholder

The library I'm using: "com.google.accompanist:accompanist-placeholder-material:0.23.1"
I want to display a placeholder in the place of (or over) a component when it's in the loading state.
I do the following for a Text:
MaterialTheme() {
var placeholderVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
placeholderVisible = !placeholderVisible
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.border(1.dp, Color.Red)
.padding(16.dp)
) {
Text(
modifier = Modifier
.then(
if (placeholderVisible) {
Modifier.height(28.dp).width(62.dp)
} else {
Modifier
}
)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello"
)
}
}
}
And I get this:
I want instead that no matter how big I set the placeholder's height or width, it will not participate in any way in the measuring process and, if I want to, to be able to draw itself even over other components (in this case let's say the red border).
As an effect of what I want, the box with red border will always have the dimension as if that Modifier.height(28.dp).width(62.dp) is not there.
I know I can draw outside a component's borders using drawWithContent, specifying the size of a rectangle or a circle (or whatever) to be component's size + x.dp.toPx() (or something like that). But how do I do this with Modifier.placeholder?
Ideally, I would need something like Modifier.placeholder(height = 28.dp, width = 62.dp)
So, with or without this ideal Modifier, the UI should never change (except, of course, the shimmer box that may be present or not).
I think I can pull this off by modifying the source code of this Modifier, but I hope I won't need to turn to that.
Just replace your Text() with below code, maybe conditional Modifier is the issue in above code!
Text(
modifier = Modifier
.size(width = 62.dp, height = 28.dp)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello",
textAlign = TextAlign.Center
)

Why are the modifier sizes not overwritten?

I have a composable function it assigns a size to a surface.
#Composable
private fun CreateImage(modifier: Modifier = Modifier) {
Surface(
modifier = modifier
.size(150.dp)
.padding(5.dp),
shape = CircleShape,
border = BorderStroke(1.dp, Color.LightGray)
) {
Image(
painter = painterResource(id = R.drawable.profile_image),
contentDescription = "Profile Image",
contentScale = ContentScale.Crop
)
}
}
When I call another method and change the size in the modifier parameter shouldn't it stick to 150dp.
If I call this method:
#Composable
private fun ChangeSize(name: String) {
CreateImage(Modifier.size(100.dp))
}
It stays with the size 100dp even though in CreateImage I set it to 150dp. Why is the size not changing to 150dp and staying 100dp?
I thought it was supposed to change it to 150dp. Why is that not the case?
Modifier uses first size it got and it's a great feature when people don't provide any size to your Composable.
For instance
CircleDisplay(
modifier = circleModifier
.widthIn(min = 70.dp)
.heightIn(min = 70.dp),
color = color
)
if anyone doesn't provide any Modifier with any width or height instead of having 0 height and width you give a minimum size to your Composable. You can change this to max or exact size depending on your implementation. But when user modifier has some width height instead of yours the one provided by them is used thanks to using first size.
Default Composables like Slider also use this pattern, so without setting any dimension it has 48.dp height and fills max width of its parent.
BoxWithConstraint under Slider is as
BoxWithConstraints(
modifier
.minimumTouchTargetSize()
.requiredSizeIn(minWidth = ThumbRadius * 2, minHeight = ThumbRadius * 2)
.sliderSemantics(value, tickFractions, enabled, onValueChange, valueRange, steps)
.focusable(enabled, interactionSource)
) {
// Rest of the Slider Implementation
}
The reason why your composable will always have a size of 150.dp is because of how modifiers are applied. When modifiers are chained onto a composable, they are applied sequentially from top to bottom until the last modifier is applied. This can be demonstrated with a very simple Box composable
#Composable
fun Screen(){
Box(
modifier = Modifier
.size(100.dp)
.background(color = Color.Yellow)
.background(color = Color.Green)
.background(color = Color.Red)
)
In this simple Box composable, the rendered color will be Color.Red because it will be the last color applied before it gets drawn onto the screen.
A similar thing is happening in your example above. Even though you are calling your composable with a modifier of 100.dp, the final size that get's applied is 150.dp because your modifier with 100.dp get's applied too early in the modifier chain.
Replace your composable with this one and it should work as expected
#Composable
private fun CreateImage(modifier: Modifier = Modifier) {
Surface(
modifier = Modifier
.size(150.dp)
.padding(5.dp)
.then(modifier), //this last modifier will override everything above
shape = CircleShape,
border = BorderStroke(1.dp, Color.LightGray)
) {
Image(
painter = painterResource(id = R.drawable.profile_image),
contentDescription = "Profile Image",
contentScale = ContentScale.Crop
)
}
}

How to resize an item in a Compose Column, depending on another item

I have a "simple" layout in Compose, where in a Column there are two elements:
top image
a grid of 4 squares underneath, each square containing some text
I'd like the layout to have the following behaviour:
set the maximum height of the image to be screenWidth
set the minimum height of the image to be 200.dp
the image should always be in a square container (cropping of the image is fine)
let the grid "grow" as much as it needs to, to wrap around the content, making the image shrink as necessary
This means that if the text in the squares is short, the image will cover a large square on the top. But if any of the square text is really, long, I want the whole grid to scale up and shrink the image. These are the desirable outcomes:
When text is short enough
When a piece of text is really long
I have tried this with ConstraintLayout in Compose, but I can't get the squares to scale properly.
With a Column, I can't get the options to grow with large content - the text just gets truncated and the image remains a massive square.
These are the components I'd built:
// the screen
Column {
Box(modifier = Modifier
.heightIn(min = 200.dp, max = screenWidth)
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.align(Alignment.CenterHorizontally),
) {
Image(
painter = painterResource(id = R.drawable.puppy),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
OptionsGrid(choicesList, modifier = Modifier.heightIn(max = screenHeight - 200.dp))
}
#Composable
fun OptionsGrid(choicesList: List<List<String>>, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, Color.Blue)
.padding(top = 4.dp, bottom = 4.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
choicesList.forEach { choicesPair ->
Row(modifier = Modifier.weight(0.5f)) {
choicesPair.forEach { choice ->
Box(
modifier = Modifier
.padding(4.dp)
.background(Color.White)
.weight(0.5f)
) {
Option(choice = choice)
}
}
}
}
}
}
#Composable
fun Option(choice: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
.border(BorderStroke(1.dp, Color.Red)),
contentAlignment = Alignment.Center
) {
Text(
text = choice,
modifier = Modifier.padding(8.dp),
textAlign = TextAlign.Center,
)
}
}
Do I need a custom layout for this? I suppose what's happening here is that the Column is measuring the image first, letting it be its maximum height, because there is space for that on the screen, and then when measuring the grid, it gives it the remaining space and nothing more.
So I'd need a layout which starts measuring from the bottom?
Here's how you can do it without custom layout.
You need your image size to be calculated after OptionsGrid. In this case you can use Modifier.weight(1f, fill = false): it forces all the views without Modifier.weight to be layout before any weighted elements.
Modifier.weight will override your Modifier.heightIn, but we can restrict it size from the other side: using Modifier.layout on OptionsGrid. Using this modifier we can override constraints applied to the view.
p.s. Modifier.heightIn(max = screenWidth) is redundant, as views are not gonna grow more than screen size anyway, unless the width constraint is overridden, for example, with a scroll view.
.height(IntrinsicSize.Min) will stop OptionsGrid from growing more than needed. Note that is should be placed after Modifier.layout, as it sets height constraint to infinity. See why modifiers order matters.
val choicesList = listOf(
listOf(
LoremIpsum(if (flag) 100 else 1).values.first(),
"Short stuff",
),
listOf(
"Medium length text",
"Hi",
),
)
Column {
Box(
modifier = Modifier
.weight(1f, fill = false)
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.align(Alignment.CenterHorizontally)
) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
OptionsGrid(
choicesList,
modifier = Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints.copy(
// left 200.dp for min image height
maxHeight = constraints.maxHeight - 200.dp.roundToPx(),
// occupy all height except full image square in case of smaller text
minHeight = constraints.maxHeight - constraints.maxWidth,
))
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
.height(IntrinsicSize.Min)
)
}
Result:
I suppose what's happening here is that the Column is measuring the
image first, letting it be its maximum height, because there is space
for that on the screen, and then when measuring the grid, it gives it
the remaining space and nothing more.
That is correct, it goes down the UI tree, measures the first child of the column(the box with the image) and since the image doesn't have any children, it returns it's size to the parent Column.
(see documentation)
I'm pretty sure this requieres a custom layout, so this is what I came up with:
First, modified your composables a bit for testing purposes (tweaked some modifiers and replaced the Texts with TextFields to be able to see how the UI reacts)
#ExperimentalComposeUiApi
#Composable
fun theImage() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.background(Color.Blue)
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.border(BorderStroke(2.dp, Color.Cyan))
)
}
}
#Composable
fun OptionsGrid(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, Color.Blue)
.padding(top = 4.dp, bottom = 4.dp)
.height(IntrinsicSize.Min),
verticalArrangement = Arrangement.Center
) {
repeat(2){
Row(modifier = Modifier.weight(0.5f)) {
repeat(2){
Box(
modifier = Modifier
.padding(4.dp)
.background(Color.White)
.weight(0.5f)
.wrapContentHeight()
) {
Option()
}
}
}
}
}
}
#Composable
fun Option() {
var theText by rememberSaveable { mutableStateOf("a")}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
.border(BorderStroke(1.dp, Color.Red)),
contentAlignment = Alignment.Center
) {
OutlinedTextField(value = theText, onValueChange = {theText = it})
}
}
And now, the custom layout
Since subcompose needs a slotId, and you only need IDs for the image and grid, you can create an Enum class with two ids.
enum class SlotsEnum {Main, Dependent}
slotID: A unique id which represents the slot we are composing into.
If you have fixed amount or slots you can use enums as slot ids, or if
you have a list of items maybe an index in the list or some other
unique key can work. To be able to correctly match the content between
remeasures you should provide the object which is equals to the one
you used during the previous measuring. content - the composable
content which defines the slot. It could emit multiple layouts, in
this case the returned list of Measurables will have multiple
elements.
Then, with this composable, which receives a screen width, height, an optional modifier and the image, as well as the grid
#Composable
fun DynamicColumn(
screenWidth: Int,
screenHeight: Int,
modifier: Modifier = Modifier,
img: #Composable () -> Unit,
squares: #Composable () -> Unit
)
You can measure the total height of the grid and use that to calculate the height of the image (still haven't managed to a proper UI when scaled under 200dp, but it shouldn't be diffcult).
SubcomposeLayout { constraints ->
val placeableSquares = subcompose(SlotsEnum.Main, squares).map {
it.measure(constraints)
}
val squaresHeight = placeableSquares.sumOf { it.height }
val remainingHeight = screenHeight - squaresHeight
val imgMaxHeight = if (remainingHeight > screenWidth) screenWidth else remainingHeight
val placeableImage = subcompose(SlotsEnum.Dependent, img).map{
it.measure(Constraints(200, screenWidth, imgMaxHeight, imgMaxHeight))
}
Then, apply the constraints to the image and finally place the items.
layout(constraints.maxWidth, constraints.maxHeight) {
var yPos = 0
placeableImage.forEach{
it.place(x= screenWidth / 2 - it.width / 2, y= yPos)
yPos += it.height
}
placeableSquares.forEach{
it.place(x=0, y=yPos)
}
}
and finally, just call the previous composable, DynamicColumn:
#ExperimentalComposeUiApi
#Composable
fun ImageAndSquaresLayout(screenWidth: Int, screenHeight: Int) {
DynamicColumn(screenWidth = screenWidth, screenHeight = screenHeight,
img = { theImage() },
squares = { OptionsGrid() })
}
PS: possibly will update this tomorrow if I can fix the minimum width issue

How do we get the position/size of a Composable in a screen?

In Compose, how do we get the position or size of a Composable in a screen ? For example, I'm trying to focus the map camera between specific bounds and adding padding. Here I need to get the padding corresponding to the pager top position and TopBar bottom position.
Currently the code of this screen is the following:
BoxWithConstraints {
MapViewWithMarkers(...)
TopAppBar(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding(),
backgroundColor = Color.Transparent,
elevation = 0.dp,
)
HorizontalPager(
state = pagerState,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.navigationBarsPadding()
.padding(bottom = 32.dp),
itemSpacing = 8.dp,
) { page ->
val hikeOnMapCard = hikeMarkerList[page]
HikeOnMapCard(hikeOnMapCard) {
viewModel.hikeOnMapCardClicked(hikeOnMapCard.id)
}
}
}
I would like to forward to the MapViewWithMarkers Composable the padding corresponding to the TopAppBar size and Pager size on this screen
Thanks !
To get the position and the size of a composable you can use the onGloballyPositioned modifier.
Something like:
var sizeTopBar by remember { mutableStateOf(IntSize.Zero) }
var positionInRootTopBar by remember { mutableStateOf(Offset.Zero) }
TopAppBar(
modifier = Modifier
.onGloballyPositioned { coordinates ->
// size
sizeTopBar = coordinates.size
// global position (local also available)
positionInRootTopBar = coordinates.positionInRoot()
}
//...
)
With complex layout to measure and layout multiple composables, use the Layout composable instead. This composable allows you to measure and lay out children manually.

Is it possible to "Scale" width and height any widget in jetpack compose?

I have a Stack widget which hosts a Box and an Image.
As the state changes, I want to scale the Box widget by whatever value the state has, for example by 2x.
I couldn't find anything about scaling the widgets on the Modifier or Box properties so I decided to react to the state changes by manipulating the size using "Modifier.size" which is not ideal for me.
So is there support for scaling the widgets or should I manually play with the size property?
-Thanks
#Composable
fun Pointer(modifier: Modifier = Modifier, state: TransitionState, onClick: () -> Unit) {
Stack(modifier) {
Box(
shape = CircleShape, backgroundColor = Color.Gray.copy(alpha = .3f),
modifier = Modifier.size(state[width])
)
Image(
asset = imageResource(id = R.drawable.ic_pointer),
modifier = Modifier
.clickable(onClick = onClick)
)
}
}
Compose 1.0.0-alpha08
As time passes by, there is a new compose version which renames 'drawLayer' to 'graphicsLayer' and adds a new scale modifier (uses 'graphicsLayer' underneath).
Thus composable will look like:
#Composable
fun Pointer(scale: Float, modifier: Modifier = Modifier) {
Box(modifier) {
Box(
modifier = Modifier
.matchParentSize()
.scale(scale)
.background(Color.Cyan, CircleShape)
)
Image(
imageVector = Icons.Filled.Done,
modifier = Modifier
.align(Alignment.Center)
)
}
}
Compose 1.0.0-alpha07 (original answer)
I believe you may achive the desired behavior with drawLayer modifier. For example a simple composable, which displays an scalable circle and an unscalable icon on top of it:
#Composable
fun Pointer(scale: Float, modifier: Modifier = Modifier) {
Box(modifier) {
Box(
modifier = Modifier
.matchParentSize()
.drawLayer(scaleX = scale, scaleY = scale)
.background(Color.Cyan, CircleShape)
)
Image(
asset = Icons.Filled.Done,
modifier = Modifier
.align(Alignment.Center)
)
}
}
And usage:
Pointer(
scale = 1f,
modifier = Modifier
.background(Color.Magenta)
.padding(25.dp)
.preferredSize(50.dp)
.align(Alignment.CenterHorizontally)
)
When scale = 1f
When scale = 2f
I don't want to care about the size of the box or explicitly keep a reference to it
That is going to be a problem. In Compose, widgets like Box() are stateless functions. You cannot ask a Box() how big it is — instead, you need to tell the Box() how big it is, using a suitable Modifier.
Frequently, "how big it is" is a fixed value or rule, set in the code (e.g., Modifier.size(200.dp)). You should be able to have the size be dependent upon some state that you track yourself, so long as that state is a State, so Compose knows to recompose (call your function again) when that State changes. If you go that route, then scaling is a matter of checking the current State value, applying your scale factor, and using the result for the new State value.

Categories

Resources