Why does scaling the text in a row changes the offset of the text view in compose? [duplicate] - android-jetpack-compose

I know how to align text in Jetpack Compose to the baseline.
But now I would need to align two differently sized texts that follow each other in a Row by the ascent of the larger of these two fonts. I would like to think of this as aligning two texts "by the top baseline" if that makes sense. Modifier.align(Alignment.Top) does not work as it will not align by the ascent but by the layout's top and then the texts are not aligned correctly at the top.
I have tried to look how to do this, but apparently there's no ready made function or modifier for this? I didn't even find a way to access Text's ascent property etc in Compose. So not sure how this would be possible?
Thanks for any hints! :)
Edit: This is what it looks when Alignment.Top is used. But I would like the two texts to align at the top.

All information about text layout can be retrieved with onTextLayout Text argument. In this case you need a line size, which can be retrieved with getLineBottom, and an actual font size, which can be found in layoutInput.style.fontSize.
I agree that it'd be easier if you could use some simple way to do that, so I've starred your feature request, but for now here's how you can calculate it:
onTextLayout = { textLayoutResult ->
val ascent = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
with(density) {
style.fontSize.toPx()
}
}
},
Full example of aligning two texts:
val ascents = remember { mutableStateMapOf<Int, Float>() }
val texts = remember {
listOf(
"Big text" to 80.sp,
"Small text" to 20.sp,
)
}
Row(
Modifier
.drawBehind {
ascents.maxOfOrNull { it.value }?.let {
drawLine(Color.Red, Offset(0f, it), Offset(size.width, it))
}
}
) {
texts.forEachIndexed { i, info ->
Text(
info.first,
fontSize = info.second,
onTextLayout = { textLayoutResult ->
ascents[i] = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
with(density) {
style.fontSize.toPx()
}
}
},
modifier = Modifier
.alpha(if (ascents.count() == texts.count()) 1f else 0f)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val maxAscent = ascents.maxOfOrNull { it.value } ?: 0f
val ascent = ascents[i] ?: 0f
val yOffset = if (maxAscent == ascent) 0 else (maxAscent - ascent).toInt()
layout(placeable.width, placeable.height - yOffset) {
placeable.place(0, yOffset)
}
}
)
}
}
Result:

One workaround would be you can adjust y-axis offset modifier according to your need.
Text(text = "Second", modifier = Modifier.offset(x = 0.dp, y = 5.dp))
you can have negative value for offset as well if you like to up your first text according to your need.

Another option is to use ConstraintLayout. You can simply constrain the tops of the two texts https://developer.android.com/jetpack/compose/layouts/constraintlayout

in addition to one of the previous answers
ascent and descent
Text(
modifier = modifier,
text = text,
onTextLayout = { result ->
val layoutInput = result.layoutInput
val fontSize = with(layoutInput.density) { layoutInput.style.fontSize.toPx() }
val lineHeight = with(layoutInput.density) { layoutInput.style.lineHeight.toPx() }
var baseline = result.firstBaseline
(0 until result.lineCount).forEach { index ->
val top = result.getLineTop(index)
val bottom = result.getLineBottom(index)
val ascent = bottom - fontSize
val descent = bottom - (baseline - fontSize - top)
baseline += lineHeight
}
}
)

Related

Why the Row in Card out of the scope of Card with fillMaxWidth()

My code:
#ExperimentalMaterial3Api
#Composable
fun Settings() {
Card(modifier = Modifier
.fillMaxWidth()
) {
SettingItem(itemTitle = "Test Title", itemDescription = "Description")
}
}
#ExperimentalMaterial3Api
#Composable
fun SettingItem(itemTitle: String, itemDescription: String) {
Row(modifier = Modifier.padding(12.dp).fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(text = itemTitle, color = MaterialTheme.colorScheme.primary)
Text(text = itemDescription, color = MaterialTheme.colorScheme.secondary)
}
Spacer(modifier = Modifier.size(width = 12.dp, height = 4.dp))
Switch(checked = false, onCheckedChange = {})
}
}
I want to put a Switch at the end of a SettingItem, so I set fillMaxWidth(). But the Switch shows out of the layout of Card.
Image
Change the Column modifier to weight.
From,
modifier = Modifier.fillMaxWidth()
To
modifier = Modifier.weight(1F)
The fillMaxWidth modifier is used to make the Column as wide as possible. Whereas here the requirement is to fill as wide as possible leaving space for the Spacer and Switch. That is achieved by using the weight modifier.
Weight
The parent will divide the horizontal space remaining after measuring unweighted child elements and distribute it according to this weight.
FillMaxWidth
Have the content fill (possibly only partially) the Constraints.maxWidth of the incoming measurement constraints, by setting the minimum width and the maximum width to be equal to the maximum width multiplied by fraction.
Note that, by default, the fraction is 1, so the modifier will make the content fill the whole available width.
Reference
weight
fillMaxWidth

android compose lazy column separate by weight

Is it possible to do weights in Jetpack Compose with lazy column?
I'd like to set it menu item is weighted as 1/n (n = number of menus) of a layout, and the other takes up the remaining 1/n, also.
I want to list it at the same height as the number of menus.
MenuList
#Composable
fun MenuList(
loading: Boolean,
menus: List<Menu>,
onNavigateToMenuDetailScreen: (String) -> Unit
) {
Box(modifier = Modifier
.background(color = MaterialTheme.colors.surface)
.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
if (loading && menus.isEmpty()) {
LoadingShimmer(imageHeight = 800.dp)
}
else if (menus.isEmpty()) {
NothingHere()
}
else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1F)
) {
itemsIndexed(
items = menus
) { index, menu ->
MenuCard(
menu = menu,
onClick = {
}
)
}
}
}
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = Modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column {
Text(
text = menu.name,
fontSize = 30.sp,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.wrapContentHeight(Alignment.CenterVertically),
style = MaterialTheme.typography.h3
)
}
}
}
In summary, MenuCards are created as many as the number of menu on the MenuList screen, and I hope the height of each MenuCard will be 1/n.
(n = number of menu)
Like, when number of menu is 8,
Just same height to each menu.
Replace your LazyColumn code with BoxWithConstraints and regular Column.
BoxWithConstraints for getting the minHeight or in this case you can say screen height.
Change to something like the below.
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val height = minHeight/menus.size
Column(
modifier = Modifier
.fillMaxSize()
) {
menus.forEachIndexed { index, menu ->
MenuCard(
modifier = Modifier.height(height),
menu = menu,
onClick = {
}
)
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column(
modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = menu.name,
fontSize = 30.sp,
style = MaterialTheme.typography.h3
)
}
}
}
You will get this
3 items and 6 items
If all you want is a way to divide the screen height in equal proportions to all the n items, you can simply go for a column.
val items = ... //I assume you have access to this
val screenHeight = LocalConfiguration.current.screenHeightDp
Column{
items.forEach{
MenuCard(modifier = Modifier.height(1f / items.size * screenHeight))
}
}
Column and LazyColumn are fundamentally different. LazyColumn's sole purpose is to deal with large datasets by loading and caching only a small window off that set. Now, if there is nothing to be scrolled, LazyColumn is useless, in fact, it is worse.
EDIT:
You could use BoxWithConstraints to get the screen dimensions as well, but it, in this context, would be more of a workaround than a solution. It adds an extra Composable and decreases the readability of the code too, so for getting the screen dimensions, always use the API that was specifically built for the job.

is there a way to change the background color of a specific word in outlined text field jetpack compose?

I am looking for a way to change the background color of certain words in outlined text field while typing, but I couldn't find a way to achieve that yet. Does anyone know a way to do this? (Preferably in compose but other solutions are also OK.)
You can use the VisualTransformation property in the TextField to change the text while typing. Use an AnnotatedString to apply different SpanStyle to your text.
Something like:
OutlinedTextField(
value = text,
onValueChange = { text = it },
visualTransformation = ColorsTransformation()
)
class ColorsTransformation() : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
buildAnnotatedStringWithColors(text.toString()),
OffsetMapping.Identity)
}
}
In the buildAnnotatedStringWithColors you can move your logic to change the background color of certain words. It is just an example to change the color for each word.
fun buildAnnotatedStringWithColors(text:String): AnnotatedString{
//it is just an example
val words: List<String> = text.split("\\s+".toRegex())// splits by whitespace
val builder = AnnotatedString.Builder()
val colors = listOf(Color.Red,Color.Black,Color.Yellow,Color.Blue)
var count = 0
for (word in words) {
//Use you favorite SpanStyle
builder.withStyle(style = SpanStyle(
background = colors[count % 4],
color = White
)) {
append("$word ")
}
count ++
}
return builder.toAnnotatedString()
}
The simplest solution is using annotated string:
Text(buildAnnotatedString {
append("Hello ")
withStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
color = Color.White,
background = Color.Blue,
)
) {
append("World")
}
}, modifier = Modifier.padding(10.dp))
Result
Also take a look at this solution, which will allow you to use a more complex drawing like this:

How to make the width of a Text equal to the width of another implicitly measured Text?

There is a reader app with two Texts, one for the total number of pages and one for the current page. I want the width of the current page Text to be equal to the width of the total page Text, but the width of the total page Text is measured implicitly.
Currently, I can only specify the width like this:
Row {
Text(modifier = Modifier.width(32.dp), text = "1000") // total page
Slider(...)
Text(modifier = Modifier.width(32.dp), text = "1") // current page
}
To get render text layout, you can use onTextLayout. It has all information you may need, including size.
Then you can pass this size to second Text modifier:
var totalPageTextWidth by remember { mutableStateOf<Int?>(null) }
val widthModifier = totalPageTextWidth?.let { width ->
with(LocalDensity.current) {
Modifier.width(width.toDp())
}
} ?: Modifier
Text(
text = "1000",
onTextLayout = { totalPageTextWidth = it.size.width }
)
Text(
text = "1",
modifier = widthModifier
)

Custom layout draws position 0 off screen when content is larger than screen

I'm trying to write a calendar/excel like screen where you can diagonally scroll the content and the column/row headers stay in place.
It's using a custom layout with placeables, where the layout is the whole drawable space, and then the events are drawn within the box where they are needed.
For example
Column 0 starts at X=0 Y=0, and each column is some width. So column 1 draws at 0, column 2 at 20, column 3 at 40 etc.
It all works correctly until the content size becomes larger than the visible width of the screen, the content that starts at position 0 starts getting drawn off screen, such as at -20. Almost like it's centering the content...
Here's some pics of whats happening when the size fits and then doesn't fit
I think it might have something to do with alignment lines in the custom layout but if it does i can't figure out what it wants me to do... I've tried alignments on the column set to start as well to no avail.
Here's a bare bones example of the problem
#Composable
fun ScheduleTest(columnList: List<String>) {
val headerWidth = 150
Column {
HeaderTest(columnList, headerWidth)
}
}
#Composable
fun HeaderTest(
columnList: List<String>,
headerWidth: Int,
headerContent: #Composable (title: String) -> Unit = { title ->
Text(title)
},
) {
val numberOfHeaders = columnList.count()
Layout(
content = {
columnList.forEach { header ->
Box {
headerContent(header)
}
}
}
) { measureables, constraints ->
val height = 45.dp.roundToPx()
val totalWidth = headerWidth * numberOfHeaders
val placeWithHeader = measureables.map { measurable ->
val placeable = measurable.measure(
constraints.copy(
minWidth = headerWidth,
maxWidth = headerWidth,
minHeight = height,
maxHeight = height,
)
)
placeable
}
layout(width = totalWidth, height = height) {
placeWithHeader.forEachIndexed { index, placeable ->
val offset = index * headerWidth
placeable.place(offset, 0)
}
}
}
}
I was helped with this on another forum but they didn't answer it here.
I wasn't constraining the main layouts width and height
val height = 45.dp.roundToPx()
val totalWidth = headerWidth * numberOfHeaders
the contract of layout is that the width/height passed to layout must
be in range of the constraints, if you violate that, your measurable
gets clamped to the constraints and your placement is centered in the
clamped space where your parent placed you
I needed to add constraints to it
val height = 45.dp.roundToPx().coerceIn(constraints.minHeight, constraints.maxHeight)
val totalWidth = headerWidth * numberOfHeaders.coerceIn(constraints.minWidth, constraints.maxWidth)

Resources