I have a Composable as follows:
#Composable
private fun MoviePosterWithRating(movie: MovieModel) {
Box {
Image(<...>)
Box( //Rating circle
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(end = 8.dp, top = 220.dp)
.size(48.dp)
.background(Color.Black, shape = CircleShape)
.align(Alignment.TopEnd)
) {
CircularProgressIndicator(
progress = movie.score / 10,
color = percentageCircleColor(movie.score),
strokeWidth = 2.dp
)
Text(
text = "${movie.score.asPercentage()}%",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 13.sp,
modifier = Modifier.padding(4.dp)
)
}
}
I would like to extract the rating circle into it's own method so I can reuse it. However, I can't because of the align on modifier. I could pass the whole modifier in as a parameter, but I would just be passing the same padding, size and background colour every time. Is there a way that I could just pass in the .align part of the modifier?
The way you should do this is to have your composable accept a Modifier as parameter, that way you can pass it at the calling point, making your composable more flexible:
#Composable
fun RatingCircle(
modifier: Modifier = Modifier,
// other attributes
) {
Box(
modifier = modifier,
) {
// other composables
}
}
Then you call it like so
Box {
Image(<...>)
RatingCircle(
modifier = Modifier.align(/* alignment */)
)
}
Related
I am trying to overlap two different compose elements. I want to show a toast kind of message at the top whenever there is an error message. I don't want to use a third party lib for such an easy use case. I plan to use the toast in every other composable screen for displaying error message. Below is the layout which i want to achieve
So I want to achieve the toast message saying "Invalid PIN, please try again".
#Composable
fun MyToast(title: String) {
Card(
modifier = Modifier
.absoluteOffset(x = 0.dp, y = 40.dp)
.background(
color = MaterialTheme.colors.primaryVariant,
shape = RoundedCornerShape(10.dp)
), elevation = 20.dp
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.colors.primaryVariant)
.padding(12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.error_circle),
contentDescription = title
)
Text(
text = title,
fontFamily = FontFamily(Font(R.font.inter_medium)),
fontSize = 12.sp,
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 10.dp)
)
}
}
}
and my screen composable is as follows
#Composable
fun Registration(navController: NavController, registrationViewModel: RegistrationViewModel) {
Scaffold() {
Box(){
MyToast(
title = "Invalid pin, please try again"
)
Column() {
//my other screen components
}
}
}
I will add the AnimatedVisibility modifier later to MyToast composable. First I need to overlap MyToast over all the other elements and somehow MyToast is just not visible
If you want the child of a Box to overlay/overlap its siblings behind, you should put it at the last part in the code
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier.background(Color.Red).size(150.dp)
)
// your Toast
Box(
modifier = Modifier.background(Color.Green).size(80.dp)
)
}
So if I put the green box before the bigger red box like this
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// your Toast
Box(
modifier = Modifier.background(Color.Green).size(80.dp)
)
Box(
modifier = Modifier.background(Color.Red).size(150.dp)
)
}
the green box will hide behind the red one
You have to solutions, you can either put the Toast in the bottom of your code because order matters in compose:
#Composable
fun Registration(navController: NavController, registrationViewModel: RegistrationViewModel) {
Scaffold() {
Box() {
Column() {
//my other screen components
}
MyToast(
title = "Invalid pin, please try again"
)
}
}
}
Or you can keep it as it is, but add zIndex to the Toast:
#Composable
fun MyToast(title: String) {
Card(
modifier = Modifier
.absoluteOffset(x = 0.dp, y = 40.dp)
.zIndex(10f) // add z index here
.background(
color = MaterialTheme.colors.primaryVariant,
shape = RoundedCornerShape(10.dp)
), elevation = 20.dp
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.colors.primaryVariant)
.padding(12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.error_circle),
contentDescription = title
)
Text(
text = title,
fontFamily = FontFamily(Font(R.font.inter_medium)),
fontSize = 12.sp,
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 10.dp)
)
}
}
}
Note: elevation in Card composable is not the same as elevation in XML so it's not going to make the composable in the top, it will just add a shadow but if you want to give the composable a higher z order use Modifier.zIndex(10f)
Recently, I try to migrate to Jetpack compose.
So, I want to show 'card' where has 'indicator' while loading
but I can't change 'indicator' size in 'SubcomposeAsyncImage'
#Composable
private fun SponsorDialogContent(
imgUrl: String,
) {
Card(
modifier = Modifier
.width(300.dp)
.wrapContentHeight(),
elevation = CardDefaults.cardElevation(0.dp),
shape = RoundedCornerShape(10.dp)
) {
Box() {
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 200.dp),
model = imgUrl,
contentDescription = null,
loading = {
CircularProgressIndicator(modifier = Modifier.size(30.dp).align(Alignment.Center), progress = 1f)
},
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth
)
CircularProgressIndicator(1f, Modifier.width(30.dp).align(Alignment.Center))
}
}
}
So. I have try the code below
#Composable
private fun SponsorDialogContent(
imgUrl: String,
) {
Card(
modifier = Modifier
.width(300.dp)
.wrapContentHeight(),
elevation = CardDefaults.cardElevation(0.dp),
shape = RoundedCornerShape(10.dp)
) {
Box() {
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 200.dp),
model = imgUrl,
contentDescription = null,
loading = {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
},
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth
)
}
}
}
Then, works well, but I don't know why
Does anyone know why?
I tried to find how to work this code but failed
As #Jan Bina mentioned it has propagateMinConstraints = true which means it forces minimum constraints to its direct child only. I explained in this answer how it effects with Surface because Surface is a Box with propagateMinConstraints = true. You can try out these with Box and you will see the same outcomes
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
) {}
}
}
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier.size(200.dp)
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
) {}
}
}
In first example on Surface forces Column to have 200.dp size even though it has Modifier.size(50.dp).
In second example Box inside Column has 50.dp size because it's not a direct descendant of Surface.
In third example if we replace Surface(Box with propagateMinConstraints true) with Box it allows direct descendant to use its own constraints or dimensions.
And when you set Modifier.fillMaxWidth you set Constraints as minWidth = parent width, maxWidth = parentWidth
I answered about Constraint types here
If you browse the source code of SubcomposeAsyncImage, you will get to the point where you can see this:
BoxWithConstraints(
modifier = modifier, // this is the modifier you passed to SubcomposeAsyncImage
contentAlignment = alignment,
propagateMinConstraints = true,
) {
// image or your loading content based on state
}
So the behavior you describe is because of your modifier that has heightIn(min = 200.dp) and your progress indicator that ends up being direct child of this Box which has propagateMinConstraints = true.
I need to do a parallax implementation and in addition when user is scrolling down actionbar should appear. I am following this tutorial:
https://proandroiddev.com/parallax-in-jetpack-compose-bf521244f49
There is an implementation:
#Composable
fun HeaderBarParallaxScroll() {
val scrollState = rememberScrollState()
Box {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(500.dp)
.background(Color.White)
.graphicsLayer {
Log.e(
"scroll",
"${scrollState.value.toFloat()}, max = ${scrollState.maxValue}, ratio = ${(scrollState.value.toFloat() / scrollState.maxValue)}"
)
alpha = 1f - ((scrollState.value.toFloat() / scrollState.maxValue) * 1.5f)
translationY = 0.5f * scrollState.value
},
contentAlignment = Alignment.Center
) {
Image(
painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "tiger parallax",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
repeat(100) {
Text(
text = "MyText",
modifier = Modifier.background(
Color.White
),
style = TextStyle(
color = Color.Red,
fontSize = 24.sp
)
)
}
}
Box(
modifier = Modifier
.alpha(min(1f, (scrollState.value.toFloat() / scrollState.maxValue) * 5f))
.fillMaxWidth()
.height(60.dp)
.background(Color.Yellow),
contentAlignment = Alignment.CenterStart
) {
Text(
text = "Header bar",
modifier = Modifier.padding(horizontal = 16.dp),
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.W900,
color = Color.Black
)
)
}
}
}
Looks like everything is working as expected, however, if I change the value repeat in this block
repeat(100) {
Text(
text = "MyText",
modifier = Modifier.background(
Color.White
),
style = TextStyle(
color = Color.Red,
fontSize = 24.sp
)
)
}
instead of 100 -> 1000 parallax working slower and in order for the actionbar to appear I need to scroll like half of the list, so to put it differently parallax responsiveness depends on how many items (height) are in the content. Eg: if it is 100 it works as expected, however, if it is 1000 it works much slower...
How to make it work properly?
Don't calculate the ratio based on the scroll.maxValue, but use the screen height as the max bound. Get it in pixels like this.
val screenHeight = with (LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.roundToPx() }
These are all composables, so you'll need to call this logic within a Composable scope. After the initialization, of course, the variable can be used anywhere, even in non-composable scopes.
I want to align the button in the BottomEnd of the Box in Row (In the BottonEnd of the bellow Card). I have Card with Row who is devided in two parts - Card and Box, and I want the Box to fill max of the rest of the row. I cannot implement it how I would wanted. Bellow I attached the visualization of the current code.
.
#Composable
fun ProductItem(product: ProductModel, onItemClick: () -> Unit, onAddToCardButton: () -> Unit) {
Card(
modifier = Modifier
.fillMaxSize()
.padding(7.dp)
.clickable { onItemClick() },
shape = MaterialTheme.shapes.large,
elevation = 4.dp
) {
Row(
modifier = Modifier.fillMaxSize()
) {
Card(
modifier = Modifier
.weight(1f),
shape = MaterialTheme.shapes.small,
elevation = 2.dp
) {
Image(
painter = painterResource(id = R.drawable.ic_splash_screen),
contentDescription = "Image of ${product.name}",
)
}
Box(
modifier = Modifier
.weight(2f)
.fillMaxHeight()
.padding(6.dp)
.background(Color.Green)
) {
Column(modifier = Modifier.align(Alignment.TopStart)) {
Text(
text = "${product.number}. ${product.name}",
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h4,
)
Text(
text = product.ingredients, fontStyle = FontStyle.Italic
)
}
Button(
modifier = Modifier.align(Alignment.BottomEnd),
onClick = {
onAddToCardButton()
},
shape = RoundedCornerShape(8.dp),
) {
if (product.type == "pizza") {
Text(text = "od ${String.format("%.2f", product.price[0])} zล")
} else {
Text(text = "${String.format("%.2f", product.price[0])} zล")
}
}
}
}
}
}
I expect you display this item in LazyColumn or inside a vertical scrollable.
Modifier.fillMaxHeight doesn't work in this case, because parent height constraint is equal to infinity.
To solve this you ofc can use a static value, but in this case intrinsic measurements can be used to wrap content size.
Add Modifier.height(IntrinsicSize.Max) to your Row.
The code looks good to me at first sight, even tried it and the button is on the bottom right corner. Where do you define the height of your ProductItem card?
Maybe you can try to define it in the code you provided by changing the
.fillMaxSize()
modifier to
.fillMaxWidth()
.requiredHeight(**.dp)
at your first Card composable.
So it would look something like this:
#Composable
fun ProductItem(product: ProductModel, onItemClick: () -> Unit, onAddToCardButton: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.requiredHeight(**.dp)
.padding(7.dp)
.clickable { onItemClick() },
shape = MaterialTheme.shapes.large,
elevation = 4.dp
) {
...
}
As Phil Dukhov answered Modifier.height(IntrinsicSize.Max) works for me.
Just set the modifier to the Root element in your Row. In your case it's Card (change .fillMaxWidth() to .height(IntrinsicSize.Max)):
Card(
modifier = Modifier
.height(IntrinsicSize.Max)
.padding(7.dp)
.clickable { onItemClick() },
shape = MaterialTheme.shapes.large,
elevation = 4.dp
)
I'm doing experiments to comprehend recomposition and smart recomposition and made a sample
Sorry for the colors, they are generated with Random.nextIn() to observe recomposition visually, setting colors has no effect on recomposition, tried without changing colors either.
What's in gif is composed of three parts
Sample1
#Composable
private fun Sample1() {
Column(
modifier = Modifier
.background(getRandomColor())
.fillMaxWidth()
.padding(4.dp)
) {
var counter by remember { mutableStateOf(0) }
Text("Sample1", color = getRandomColor())
Button(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
counter++
}) {
Text("Counter: $counter", color = getRandomColor())
}
}
}
I have no questions here since smart composition works as expected, Text on top is not reading changes in counter so recomposition only occurs for Text inside Button.
Sample2
#Composable
private fun Sample2() {
Column(
modifier = Modifier.background(getRandomColor())
) {
var update1 by remember { mutableStateOf(0) }
var update2 by remember { mutableStateOf(0) }
println("ROOT")
Text("Sample2", color = getRandomColor())
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
update1++
},
shape = RoundedCornerShape(5.dp)
) {
println("๐ฅ Button1๏ธ")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update2++ },
shape = RoundedCornerShape(5.dp)
) {
println("๐ Button 2๏ธ")
Text(
text = "Update2: $update2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column(
modifier = Modifier.background(getRandomColor())
) {
println("๐ Inner Column")
var update3 by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update3++ },
shape = RoundedCornerShape(5.dp)
) {
println("โ
Button 3๏ธ")
Text(
text = "Update2: $update2, Update3: $update3",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
Column() {
println("โ๏ธ Bottom Column")
Text(
text = "Sample2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
}
It also works as expected each mutableState is updating only the scope they have been observed in. Only Text that observes update2 and update3 is changed when either of these mutableStates are updated.
Sample3
#Composable
private fun Sample3() {
Column(
modifier = Modifier.background(getRandomColor())
) {
var update1 by remember { mutableStateOf(0) }
var update2 by remember { mutableStateOf(0) }
println("ROOT")
Text("Sample3", color = getRandomColor())
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
update1++
},
shape = RoundedCornerShape(5.dp)
) {
println("๐ฅ Button1๏ธ")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update2++ },
shape = RoundedCornerShape(5.dp)
) {
println("๐ Button 2๏ธ")
Text(
text = "Update2: $update2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column {
println("๐ Inner Column")
var update3 by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update3++ },
shape = RoundedCornerShape(5.dp)
) {
println("โ
Button 3๏ธ")
Text(
text = "Update2: $update2, Update3: $update3",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
// ๐ฅ๐ฅ Reading update1 causes entire composable to recompose
Column(
modifier = Modifier.background(getRandomColor())
) {
println("โ๏ธ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
}
Only difference between Sample2 and Sample3 is Text at the bottom is reading update1 mutableState which causing entire composable to be recomposed. As you can see in gif changing update1 recomposes or changes entire color schema for Sample3.
What's the reason for recomposing entire composable?
Column(
modifier = Modifier.background(getRandomColor())
) {
println("โ๏ธ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
To have smart recomposition scopes play a pivotal role. You can check Vinay Gaba's What is โdonut-hole skippingโ in Jetpack Compose? article.
Leland Richardson explains in this tweet as
The part that is "donut hole skipping" is the fact that a new lambda
being passed into a composable (ie Button) can recompose without
recompiling the rest of it. The fact that the lambda are recompose
scopes are necessary for you to be able to do this, but not
sufficient
In other words, composable lambda are "special" :)
We wanted to do this for a long time but thought it was too
complicated until #chuckjaz had the brilliant realization that if the
lambdas were state objects, and invokes were reads, then this is
exactly the result
You can also check other answers about smart recomposition here, and here.
https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78
When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit. Column, Row and Box are inline functions and because of that they don't create scopes.
Created RandomColorColumn that take other Composables and its scope content: #Composable () -> Unit
#Composable
fun RandomColorColumn(content: #Composable () -> Unit) {
Column(
modifier = Modifier
.padding(4.dp)
.shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp))
.background(getRandomColor())
.padding(4.dp)
) {
content()
}
}
And replaced
Column(
modifier = Modifier.background(getRandomColor())
) {
println("โ๏ธ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
with
RandomColorColumn() {
println("โ๏ธ Bottom Column")
/*
๐ฅ๐ฅ Observing update(mutableState) does NOT causes entire composable to recompose
*/
Text(
text = "๐ฅ Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
Only this scope gets updated as expected and we have smart recomposition.
What causes Text, or any Composable, inside Column to not have a scope, thus being recomposed when a mutableState value changes is Column having inline keyword in function signature.
#Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: #Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
If you add inline to RandomColorColumn function signature you will see that it causes whole Composable to recompose.
Compose uses call sites defined as
The call site is the source code location in which a composable is
called. This influences its place in Composition, and therefore, the
UI tree.
If during a recomposition a composable calls different composables
than it did during the previous composition, Compose will identify
which composables were called or not called and for the composables
that were called in both compositions, Compose will avoid recomposing
them if their inputs haven't changed.
Consider the following example:
#Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // This call site affects where LoginInput is placed in Composition
}
#Composable
fun LoginInput() { /* ... */ }
Call site of a Composable function affects smart recomposition, and having inline keyword in a Composable sets its child Composables call site same level, not one level below.
For anyone interested here is the github repo to play/test recomposition