I want to display a text with an icon left to the text. The text and icon should be centered horizontally. Here is a composable function for this:
Column(Modifier.fillMaxSize()) {
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
// imagine this Box is an icon
Box(
Modifier
.size(48.dp)
.background(Color.Red)
)
Spacer(Modifier.width(8.dp))
Text(
text = "text ".repeat(3),
textAlign = TextAlign.Center,
)
}
}
It works fine with short words:
But adding long words to the text makes it too wide, and it seems that there is too much space between the icon and the text:
I've tried to add Modifier.width(IntrinsicSize.Min) to the text, and it actually solves the issue with long words:
But it breaks displaying short words:
I don't know how to make work both long and short words. Hope to get help here.
UPD:
The same result is for Android native views. Gist with xmls.
You can consider this one, all of the codes below are copy-and-paste-able.
#Composable
fun MyScreen() {
var text by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = text,
onValueChange = { text = it}
)
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.wrapContentSize()
) {
Spacer(modifier = Modifier.weight(.5f))
SomeComposable(text = text, modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.weight(.5f))
}
}
}
#Composable
fun SomeComposable(
text: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
Box(
Modifier
.align(Alignment.CenterVertically)
.size(48.dp)
.background(Color.Red)
)
Spacer(Modifier.width(8.dp))
Text(
modifier = Modifier.width(IntrinsicSize.Min),
text = text,
textAlign = TextAlign.Center,
)
}
}
I just put a Spacer between the components and weighted them accordingly.
Output:
Solution proposed by my colleague.
#Composable
fun FlowText(text: String) {
FlowRow(
mainAxisSpacing = 4.dp,
mainAxisAlignment = MainAxisAlignment.Center,
crossAxisAlignment = FlowCrossAxisAlignment.Center,
) {
text.splitToSequence(' ').filter { it.isNotEmpty() }.forEach {
Text(text = it, textAlign = TextAlign.Center)
}
}
}
Demo: https://youtu.be/WXqvxlsJ3xM
Related
I have some Column with Icon and Text inside. Column is wrap the size of Text but i want to Column wrap Icon and long text move to another line
How It's looks now
How I want it to look like
#Composable
fun ServiceItem(
service: Service,
onItemClick: (service: Service) -> Unit
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.padding(horizontal = 16.dp),
shape = RoundedCornerShape(itemBackgroundCornerSize),
backgroundColor = colorResource(id = R.color.gray)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(itemSize)
.clickable(onClick = {
onItemClick(service)
})
) {
Image(
painter = rememberAsyncImagePainter(service.imageUrl),
contentDescription = null,
modifier = Modifier.padding(iconPadding)
)
}
}
Text(
text = service.title,
textAlign = TextAlign.Center,
style = itemTitleTextDefaultStyle(),
modifier = Modifier.padding(top = textTopPadding)
)
}
}
RI am developing an Android app using jetpack compose.
Here is a very basic UI component:
I want to add a button on the right side.
But if the name is very long, the button is gone.
My code is here:
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberImagePainter(data = profileImg),
contentDescription = null,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1F) // I set the weight in here but it doesn't work.
) {
Text(
text = "very very very very very very very long name",
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = "3 minutes ago",
)
}
}
Row {
Button()
Button()
}
}
How can I show the right button correctly?
You need to actually provide that weight to Row containing your Text & make sure you don't cover the entire width. e.g don't do just 1f.
You can do something like this; (This is done with compose_version = '1.0.1')
#Composable
fun Item() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(0.7f)
) {
Image(
painter = painterResource(R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Column(
verticalArrangement = Arrangement.Center,
) {
Text(
text = "very very very very very very very long name",
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = "3 minutes ago",
)
}
}
Button(
onClick = {}, modifier = Modifier
.wrapContentWidth()
.weight(0.3f)
) {
Text(text = "Button")
}
}
}
Output:
Here is a working code (I've removed useless Row, it's simpler that way)
#Composable
fun Test() {
Row(
modifier = Modifier.fillMaxWidth,
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberImagePainter(data = profileImg),
contentDescription = null,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f) // I set the weight in here but it doesn't work.
) {
Text(
text = "very very very very very very very long name",
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = "3 minutes ago",
)
}
Button(onClick = { }) {
Text(text = "Btn1")
}
Button(onClick = { }) {
Text(text = "Btn2")
}
}
}
Even though the other question are right I am going to explain your mistake, to better understand what is going on.
So you need:
A Row() which will contain those 3:
Image
Column
and a Row with the two buttons
In other words something like:
Row() {
Image()
Column(weight:1f)
Row()
}
Your mistake is that you created a Row with two other Rows without weights and you get this weird output.
So if you simply delete your outer Row and move your Row of buttons like so it will work:
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
I'm using a HorizontalPager from accompanist package. The pager will have 1 page for each exercise stored by the user.
Each page in the pager has a LazyColumn which contain multiple cards with controls such as IconButtons and BasicTextField. The number of cards depends on number of sets configured by the user. I expect the typical number to be between 1 to 8 but only 3 to 5 would be visible on the screen at any given time (depending on the screen size and resolution).
The issue is that when this layout produces noticeable lag (animations skip frames) every time the HorizontalPager needs to build a new page that has more than 3 cards. This happens when swapping between pages. The same happens in debug and release versions running on a real device (Galaxy S10e) and emulator.
I'm trying to optimise this layout, so each frame renders in no more than 16ms regardless of the number of cards shown on the screen.
I've previously tried to solve this issue by setting fixed heights to some composables but that didn't help much. I've also tried using Text instead of BasicTextField, which would be then replaced with BasicTextField when users taps on the text but this hasn't helped much, therefore I removed this implementation.
Do you have some suggestions how performance of this layout could be improved to eliminate the lag?
Below is my code, screen shoot of the app screen and profiler:
#ExperimentalPagerApi
#Composable
fun WorkoutSessionScreen(
navHostController: NavHostController,
) {
val pagerState = rememberPagerState()
Scaffold(
topBar = { MyTopAppBar(navHostController = navHostController) }
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.background(MaterialTheme.colors.background)
.imePadding()
) {
HorizontalPager(
count = 10, state =
pagerState,
itemSpacing = 16.dp
) {
Log.e("==>", "Building horizontal pager")
TrackingControls()
}
Box(
modifier = Modifier
.height(70.dp)
.fillMaxWidth()
.background(Color(0xFAF7F7FF))
.align(Alignment.BottomCenter)
) {
Column(
Modifier.fillMaxWidth()
) {
Divider(color = Color(0x2A5C5C5C))
BottomControls()
}
}
}
}
}
#Composable
private fun TrackingControls() {
LazyColumn(
Modifier
.fillMaxHeight()
.fillMaxWidth(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(6) { item ->
SetsAndRepsTrackingControls(
modifier = Modifier
)
}
}
}
#Composable
private fun BottomControls() {
Text(text = "Bottom Controlls")
}
#Composable
fun SetsAndRepsTrackingControls(modifier: Modifier = Modifier) {
val add = painterResource(id = R.drawable.ic_round_add_24)
val remove = painterResource(id = R.drawable.ic_round_remove_24)
Card(
modifier
.fillMaxWidth()
.height(200.dp)
.padding(vertical = 16.dp, horizontal = 8.dp),
backgroundColor = MaterialTheme.colors.surface,
shape = RoundedCornerShape(12.dp),
) {
Column() {
ControlsHeader()
TrackingInput(label = "REPS", add, remove)
Divider(color = Color.LightGray)
TrackingInput(label = "WEIGHT (KG)", add, remove)
}
}
}
#Composable
private fun ControlsHeader() {
Row(
modifier = Modifier
.height(56.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.primary, RoundedCornerShape(12.dp))
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Set 1")
Text(text = "CheckBox")
}
}
#Composable
private fun TrackingInput(label: String = "Preview", add: Painter, remove: Painter) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_round_remove_24),
contentDescription = "Minus",
tint = MaterialTheme.colors.onSurface
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
BasicTextField(
singleLine = true,
value = "8",
onValueChange = {},
textStyle = TextStyle(
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface
),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = label,
style = MaterialTheme.typography.overline.copy(color = MaterialTheme.colors.onSurface)
)
}
IconButton(onClick = { Log.d("==>", "tada") }) {
Icon(
painter = painterResource(id = R.drawable.ic_round_add_24),
contentDescription = "Minus",
tint = MaterialTheme.colors.onSurface
)
}
}
}
This profiler print screen shows rendering of a single card called SetsAndRepsTrackingControls
I want to make a Row of -90 degrees rotated Text Composables to make something like below:
However this code (repro case):
#Preview(showBackground = true, backgroundColor = 0xffffffff)
#Composable
fun Preview_Row_With_Rotated_Text() {
Row {
Text(
text = "A text",
modifier = Modifier
.padding(2.dp)
.rotate(-90f),
maxLines = 1,
)
Text(
text = "A text which is a bit longer",
modifier = Modifier
.padding(2.dp)
.rotate(-90f),
maxLines = 1,
)
Text(
text = "A text which is kinda longer than previous one",
modifier = Modifier
.padding(2.dp)
.rotate(-90f),
maxLines = 1,
)
}
}
produces this:
The Row uses old width of Text composables (the width of non-rotated Text) to place them one after another.
Where is the reason of this problem?
You can try using it in a Column and then rotating that Column by -90f:
#Composable
fun Main() {
Column(
modifier = Modifier
.width(200.dp)
.rotate(-90f)
) {
MyText(text = "Financial Advice")
MyText(text = "Strategy and Marketing")
MyText(text = "Information Technology")
}
}
#Composable
fun MyText(text: String) {
Text(
text = text,
modifier = Modifier
.padding(4.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.secondary)
.padding(16.dp),
textAlign = TextAlign.Center,
maxLines = 1
)
}