Wrap Content in Jetpack Compose - android

How to make the parent layout - Box wrap its content in Jetpack compose?
The current implementation below fills the entire screen, I only want the Box to wrap around its child - Switch. How do I define wrap content for the Box?
#Composable
fun TestScreen(modifier: Modifier = Modifier) {
Box(modifier = Modifier.background(Color.Yellow)){
val switchState = remember { mutableStateOf(true) }
Switch(
checked = switchState.value,
enabled= true,
onCheckedChange = { switchState.value = it }
)
}
}

What you see on screen is not about Modifier.wrapContentSize. Modifier.wrapContentSize and having no size Modifier returns same constraints. You can check out Constraints section of this answer.
Why your Box covers entire screen is probably something more sneaky you have a Surface with Modifier.fillMaxSize() as direct parent of TestScreen.
Surface is a Box with propagateMinConstraints: Boolean = true which forces minimum width and height to its direct descendent. This answer explains with examples how it works.
Your Composable is actually as this when parent is not Surface as i mentioned above.
Surface(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
TestScreen()
}
}

Since you haven't specified on what container/parent composable your posted composable is being called, I can only suggest using Modifier's wrapContentSize.
Box(
modifier = Modifier
.background(Color.Yellow)
.wrapContentSize() // wrap content height and width
){
val switchState = remember { mutableStateOf(true) }
Switch(
checked = switchState.value,
enabled= true,
onCheckedChange = { switchState.value = it }
)
}

Related

How to show semi-transparent loading overlay above full Composable

I am trying to create a Composable that wraps another content Composable and displays a CircularProgressBar as an overlay on top of it, covering the whole content Composable.
I almost got it working as wished, see the following to images:
Initial state
Loading state
But as you can see, the overlay fills the complete screen instead of just the gray LazyRow item and the text field. Thus, the button is pushed off screen.
This is my current code:
#Composable
fun LoadingBox(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
loadingText: String,
content: #Composable() () -> Unit
) {
Box(modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
) {
content()
if (isLoading) {
Surface(
modifier = Modifier
.fillMaxSize()
.alpha(0.5f),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Text(text = loadingText)
}
}
}
}
}
On the screenshots, I am providing the gray box and the text field as the content parameter. So the overlay should only cover the gray LazyRow item and the text field.
I already stumbled across instrinsic measures, however I cannot use them as the App crashes when I provide a LazyRow as content due to following error:
java.lang.IllegalStateException: Asking for intrinsic measurements of SubcomposeLayout layouts is not supported. This includes components that are built on top of SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate this:
- if intrinsic measurements are used to achieve 'match parent' sizing,, consider replacing the parent of the component with a custom layout which controls the order in which children are measured, making intrinsic measurement not needed
You should:
add contentAlignment = Alignment.Center in the parent Box
Remove the Surface
remove the verticalArrangement in the Column
Add an other Box which you can fill with a translucent background
Something like:
Box(modifier = modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
content()
if (isLoading) {
Box(
Modifier
.matchParentSize()
.background(Color.Gray.copy(alpha = 0.5f))
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material.CircularProgressIndicator()
Text(text = loadingText)
}
}
}

LazyColumn inside Column with verticalScroll modifier throws exception

In one of my composables, a Lazycolumn is nested inside a Column composable. I want to be able to scroll the entire Column along with the Lazycolumn. But, specifying the verticalScroll modifier property on Column results in the following exception causing the app to crash. How can I fix this?
Exception
java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
Composable
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = 100.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
) {
items(
items = allItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
}
...
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = {
navController.navigate(NavScreens.AddItemScreen.route) {
popUpTo(NavScreens.AddItemScreen.route) {
inclusive = true
}
}
}
) {
Text("Go to add item screen")
}
}
This happens when you wish to measure your LazyColumn with Constraints with Constraints.Infinity for the maxHeight which is not permitted as described in error log. There should be a fixed height or you shouldn't have another Scrollable with same orientation.
Column(
modifier = Modifier
// This is the cause
.verticalScroll(rememberScrollState())
) {
LazyColumn(
// and not having a Modifier that could return non-infinite max height contraint
modifier = Modifier
.fillMaxWidth()
) {
}
If you don't know exact height you can assign Modifier.weight(1f) to LazyColumn.
Contraints
This section is extra about `Constraints` and measurement you can skip this part if you are not interested in how it works.
What i mean by measuring with with Constraints.Infinity is when you create a Layout in Compose you use
Layout(modifier=modifier, content=content){
measurables: List<Measurable>, constraints: Constraints ->
}
You get child Composables as List<Measurable> which you can measure with Constraints provided by parent or the one you see fit by updating existing one with Constraints.copy or fixed one when you build a custom Composable with Layout.
val placeable = measurable.measure(constraints)
Constraints min/max width/height changes based on size modifier or scroll. When there is a scroll and you don't use any size modifier, Constraints return
minHeight =0, maxHeight= Int.MAX_VALUE as Constraints.Infinity
Modifier.fillMaxWidth()
Modifier.fillMaxWidth().weight(1f)
Easiest way to check Constraints with different Modifiers or with scroll is getting it from BoxWithConstraints

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.

Jetpack Compose find parents width/length

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")
}
}
}
}

When will Subcompose Layout support Intrinsic layout, if ever?

I see this function in the source code of androidx.compose.ui.layout.SubcomposeLayout.kt in androidx.compose.ui:ui:1.0.0-beta02.
private fun createMeasurePolicy(
block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
): MeasurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy(
error = "Intrinsic measurements are not currently supported by SubcomposeLayout"
) {
...
}
It looks like I can't use intrinsic measurement when the composable will be rendered within a subcomposable.
For reference, I'm trying to use a view like this inside a ModalBottomSheet. The intention is to have a scrollable view within the sheet, with a sticky view always at the bottom (like a button). I'd like the scrollable content to only take up as much space as it needs, and not always be full screen when in the sheets expanded state, which weight(1f) does.
Column(
modifier = Modifier
.height(IntrinsicSize.Min)
.wrapContentHeight(Alignment.Bottom),
verticalArrangement = Arrangement.Bottom
) {
Column(
content = sheetContent,
modifier = Modifier
.weight(1f)
.wrapContentHeight(Alignment.Bottom)
)
Box {
bottomStickyContent?.let { it() }
}
}
Sounds like the answer is no, SubcomposeLayout will not get Intrinsic support anytime soon, if ever.
I solved this problem by updating by code to use constraint layout.
ConstraintLayout {
val (sticky, column) = createRefs()
Column(
content = sheetContent,
modifier = Modifier
.constrainAs(column) {
top.linkTo(parent.top)
bottom.linkTo(sticky.top)
height = Dimension.preferredWrapContent
}
.wrapContentHeight(Alignment.Bottom)
)
Box(
modifier = Modifier
.constrainAs(sticky) {
bottom.linkTo(parent.bottom)
}
) {
bottomStickyContent?.let { it() }
}
}

Categories

Resources