How to put Text composables in Row, one with a mutable width - android

I want to know the way to put Text composables in Row, one with a mutable width for a TopAppBar.
Regarding the two Texts, one has a width that varies with the number of characters, and the other has a fixed number of characters and we want the string to be displayed reliably.
I tried writing codes to make that, but it did not work as I had expected.
Here is my codes and a image of what I thought.
#Composable
fun AppBarTestScreen() {
Column {
TopAppBar(
title = {
Row(Modifier.fillMaxWidth()) {
Text(
text = "〇〇〇〇〇〇",
modifier = Modifier.wrapContentWidth().widthIn(max = 210.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
Text(
text = "'s Attribute",
modifier = Modifier.fillMaxWidth().clipToBounds(),
textAlign = TextAlign.Start,
overflow = TextOverflow.Clip,
softWrap = false
)
}
}
)
....
}
}
I tried a variety of Modifier functions and patterns
For now, I set a maximum width on the first Text. However, this works well when the device is in portrait orientation, but not in landscape orientation. Furthermore, the results are not clear when it comes to tablets and other devices.
If there is no other way, I will eventually have to rely on unstable external libraries, but is there any way to make what I want without them?

Apply the weight modifier to the 1st Text:
Row(Modifier.fillMaxWidth()) {
Text(
text = "〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇",
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(text = "'s Attribute")
}
If you want to avoid to fill the whole space with the 1st Text use Modifier.weight(1f,false):
Text(
text = "〇〇〇〇〇〇〇",
modifier = Modifier.weight(1f,false),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)

Related

Compose Textfield cursor remainsfor a while after navigating to a new screen

After setting up a TextField in the TopAppBar. I encountered two problems:
Textfiled gains focused after folding keyboard. When textfield is not triggered, it looks like pic 1. And when I press it(it is focused), the keyboard shows up and tesxfield looks like pic 2. If I fold the keyboard manually, the textfiled keeps pic 2 look. I wish it can turn back to pic 1 look.
Editting cursor (the pin shape stuff) remains on the screen when I switch to a new screen. pic3 shows the cursor. The color is the same as the background. I'll change it. When I switch to another screen, it remains there like pic 4 shows. It only lasts for a split second and disappears. But it is there.
For the first problem, I must missed some parameter I gusess.
I have no idea for the second one. Is it a compose bug?
#Composable
fun SearchBar() {
var text by remember { mutableStateOf("") }
Box(
modifier = Modifier
.height(70.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.primary),
contentAlignment = Alignment.BottomCenter
) {
TextField(
leadingIcon = {
Icon(imageVector = Icons.Default.Search, contentDescription = null)
},
placeholder = {
Text(
"find more...",
color = Color.White
)
},
shape = RectangleShape,
colors = TextFieldDefaults.textFieldColors(
textColor = Color.White,
backgroundColor = MaterialTheme.colors.primary,
focusedIndicatorColor = Color.White,
unfocusedIndicatorColor = Color.Gray,
disabledIndicatorColor = Color.Black
),
value = text,
singleLine = true,
onValueChange = { text = it },
modifier = Modifier
.padding( bottom = 10.dp)
.size(360.dp,60.dp),
)
}
}
Ok thanks for the extra details, try this!
For part 1) this code can be added to the TextField to remove focus:
val focusManager = LocalFocusManager.current
TextField(
...
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide()
focusManager.clearFocus(force = true)
}),
)
Note the Done option will show a little check box in the keyboard you can use to close the keyboard and then this code will remove focus. I'm hoping onDone will also be called when manually closing the keyboard.
For part 2) I would reuse the approach. It sounds like a bug, but basically I would try calling LocalFocusManager.current.clearFocus(force = true) from where ever you are doing the navigation away from this screen, right before leaving, to force the thing to disappear.
Hope this helps!

Jetpack Compose TextField cuts text on scroll

I have a TextField with a fixed height. When the user enters a longer text it will scroll. It will cut off any text within the padding when scrolling:
Basically something like this:
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { value -> text = value },
modifier = modifier
.fillMaxWidth()
.height(100.dp),
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
backgroundColor = Color.Transparent
)
)
It is possible to adjust/remove the padding for a TextField, by using BasicTextField directly, e.g. see this stack overflow question.
However I want to keep the padding, but without the clipping of the text when the user scrolls. A simple Text Composable has this behavior.
You can use BasicTextField and modify its decorationBox parameter.
Simply put innerTextField() inside a scrollable Column and add Spacers at the top and the bottom of it.
var text by remember {
mutableStateOf("Hello Stackoverflow")
}
val paddingSize = remember { 16.dp }
BasicTextField(
modifier = Modifier
.height(100.dp),
value = text,
onValueChange = { text = it },
decorationBox = { innerTextField ->
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(state = rememberScrollState())
) {
Spacer(modifier = Modifier.height(paddingSize))
innerTextField()
Spacer(modifier = Modifier.height(paddingSize))
}
}
)
Just use the maxLines parameter of the TextField to avoid clipping. Set it to 1 or 2, per your case, then adjust the height/font-size so the max lines you specify are accomodated correctly. It will likely start to snap to the visible portion, cutting the extra stuff entirely.

How ellipsis only middle text in Android Compose?

I am developing an android app using the jetpack compose.
I want to make a UI like:
Row(
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier.weight(1F)
) {
Text(
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "($count)",
maxLines = 1,
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = timeText,
)
}
But actually it shows like:
the count information (1) is cut.
How can I show the count information and ellipsis the name text?
Should I use the compose-constraintlayout?
In such cases Modifier.weight should be used on the view, in this case it'll be measured after other siblings:
The parent will divide the vertical space remaining after measuring unweighted child elements and distribute it according to this weight.
If you don't need the view to take all the space available, add fill = false parameter.
Text(
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "($count)",
maxLines = 1,
)

Create One Larger Item in Row/Column Jetpack Compose

How to create BottomNavigation with one of the item is larger than the parent, but without using floatingActionButton. For example like this:
I tried to do that by wrapping the icon with Box but it get cut like this:
Then i try to separate that one button and use constraintLayout to position it, but the constraintLayout cover the screen like this. Even when i color it using Color.Transparent, it always feels like Color.White (i dont know why Color.Transparent never work for me). In this picture i give it Red color for clarity reason.
So how to do this kind of bottomNavBar without having to create heavy-custom-composable?
Update: so i try to make the code based on MARSK and Dharman comment (thanks btw). This is what i
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(Color.Transparent)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.White)
.align(Alignment.BottomCenter)
)
Row(
modifier = Modifier
.zIndex(56.dp.value)
.fillMaxWidth()
.selectableGroup(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
items.forEach { item ->
val selected = item == currentSection
BottomNavigationItem(
modifier = Modifier
.align(Alignment.Bottom)
.then(
Modifier.height(
if (item == HomeSection.SCAN) 84.dp else 56.dp
)
),
selected = selected,
icon = {
if (item == HomeSection.SCAN) {
ScanButton(navController = navController, visible = true)
} else {
ImageBottomBar(
icon = if (selected) item.iconOnSelected else item.icon,
description = stringResource(id = item.title)
)
}
},
label = {
Text(
text = stringResource(item.title),
color = if (selected) Color(0xFF361DC0) else LocalContentColor.current.copy(
alpha = LocalContentAlpha.current
),
style = TextStyle(
fontFamily = RavierFont,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 18.sp,
),
maxLines = 1,
)
},
onClick = {
if (item.route != currentRoute && item != HomeSection.SCAN) {
navController.navigate(item.route) {
launchSingleTop = true
restoreState = true
popUpTo(findStartDestination(navController.graph).id) {
saveState = true
}
}
}
}
)
}
}
}
It works in preview, but doesn't work when i try in app.
This one in the preview, the transparent working as expected:
And this is when i try to launch it, the transparent doesnt work:
Note: I assign that to bottomBar of Scaffold so i could access the navigation component. Is it the cause that Transparent Color doesnt work?
Update 2: so the inner paddingValues that makes the transparent doesnt work. I fixed it by set the padding bottom manually:
PaddingValues(
start = paddingValues.calculateStartPadding(
layoutDirection = LayoutDirection.Ltr
),
end = paddingValues.calculateEndPadding(
layoutDirection = LayoutDirection.Ltr
),
top = paddingValues.calculateTopPadding(),
bottom = SPACE_X7,
)
Custom Composable are not heavy, really.
Anyway, try this:-
Create a Container of MaxWidth (maybe a BoxWithConstraints or something), keep its background transparent, set the height to wrap content. Create the tabs as usual, but keeping the bigger tab's icon size bigger explicitly using Modifier.size(Bigger Size).
After you have this setup, add another container inside this container with white background, covering a specific height of the original container. Let's say 60%
Now set the z-index of all the icons and tabs to higher than the z-index of this lastly added container. Use Modifier.zIndex for this. And viola, you have your Composable ready.
In order to set a specific percentage height of the inner container, you will need access to the height of the original container. Use BoxWithConstraints for that, or just implement a simple custom Layout Composable

Specify minimal lines for Text in Jetpack Compose

For various reasons a Text should always have at least the height equal to x lines of text, no matter if it has less than x lines of text. The Text and BasicText Composables only have a maxLines parameter but no minLines
I have tried the following (x = 3):
Text(
modifier = Modifier.sizeIn(minHeight = with(LocalDensity.current) {
(42*3).sp.toDp()
}),
color = MaterialTheme.colors.onPrimary,
text = "Sample", textAlign = TextAlign.Center,
style = MaterialTheme.typography.h2 /* fontSize = 42 */,
lineHeight = 42.sp
)
The resulting height is less than if the text would contain 3 lines
Back in View World Android, we could simply use minLines=3, how can we achieve this in Jetpack Compose?
Your code is almost correct, just set lineHeight to fontSize*4/3:
var lineHeight = MaterialTheme.typography.h2.fontSize*4/3
Text(
modifier = Modifier.sizeIn(minHeight = with(LocalDensity.current) {
(lineHeight*3).toDp()
}),
color = MaterialTheme.colors.onPrimary,
text = "Sample", textAlign = TextAlign.Center,
style = MaterialTheme.typography.h2,
lineHeight = lineHeight
)
But you can do something similar without calculations using onTextLayout callback:
fun main() = Window {
var text by remember { mutableStateOf("Hello, World!") }
var lines by remember { mutableStateOf(0) }
MaterialTheme {
Button(onClick = {
text += "\nnew line"
}) {
Column {
Text(text,
maxLines = 5,
style = MaterialTheme.typography.h2,
onTextLayout = { res -> lines = res.lineCount })
for (i in lines..2) {
Text(" ", style = MaterialTheme.typography.h2)
}
}
}
}
}
While we are waiting for Google implements this feature you can use this workaround:
#Preview
#Composable
fun MinLinesPreview() {
lateinit var textLayoutResult: TextLayoutResult
val text = "Title\ntitle\nTITLE\nTitle"
// val text = "title\ntitle\ntitle\ntitle"
// val text = "title\ntitle"
// val text = "title"
Text(
modifier = Modifier.fillMaxWidth(),
text = text.addEmptyLines(3), // ensures string has at least N lines,
textAlign = TextAlign.Center,
maxLines = 4,
)
}
fun String.addEmptyLines(lines: Int) = this + "\n".repeat(lines)
Now your Text has the same height regardless string content:
This solution is much more easier than calculate Text's bottom offset based on line height in onTextLayout (spoiler: start, center and last line have different height)
If one additional recomposition of the Text is fine for you, you can also make use of the onTextLayout callback of Text as a workaround until there is official support for minimum lines from Google:
val minLineCount = 4
var text by remember { mutableStateOf(description) }
Text(
text = text,
maxLines = minLineCount, // optional, if you want the Text to always be exactly 4 lines long
overflow = TextOverflow.Ellipsis, // optional, if you want ellipsizing
textAlign = TextAlign.Center,
onTextLayout = { textLayoutResult ->
// the following causes a recomposition if there isn't enough text to occupy the minimum number of lines!
if ((textLayoutResult.lineCount) < minLineCount) {
// don't forget the space after the line break, otherwise empty lines won't get full height!
text = description + "\n ".repeat(minLineCount - textLayoutResult.lineCount)
}
},
modifier = Modifier.fillMaxWidth()
)
This will also properly work with ellipsizing and any kind of font padding, line height style etc. settings your heart desires.
A "fake" 4-line Text (with, say, 2 empty lines at the end) will have the same height like a "real" 4 line Text with 4 fully occupied lines of text. This oftentimes can be super important when e.g. laying out multiple wrap_content-height cards horizontally next to each other and the Text (in combination with maxLines) should determine the height of the cards, while all cards should have the same height (and it should work in regular and tall languages, like Burmese).
Please note, that this will not work in Android Studio's preview. My guess is, that Studio doesn't allow recompositions in the preview for performance reasons.
Below is a solution that I came up with that will set the height to a specific number of lines (you could adapt the modifier to make it minLines) It is inspired by code found from the compose SDK
// Inspiration: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt#L38
fun Modifier.minLinesHeight(
minLines: Int,
textStyle: TextStyle
) = composed {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val resolvedStyle = remember(textStyle, layoutDirection) {
resolveDefaults(textStyle, layoutDirection)
}
val resourceLoader = LocalFontLoader.current
val heightOfTextLines = remember(
density,
textStyle,
layoutDirection
) {
val lines = (EmptyTextReplacement + "\n").repeat(minLines - 1)
computeSizeForDefaultText(
style = resolvedStyle,
density = density,
text = lines,
maxLines = minLines,
resourceLoader
).height
}
val heightInDp: Dp = with(density) { heightOfTextLines.toDp() }
val heightToSet = heightInDp + OutlinedTextBoxDecoration
Modifier.height(heightToSet)
}
// Source: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt#L61
fun computeSizeForDefaultText(
style: TextStyle,
density: Density,
text: String = EmptyTextReplacement,
maxLines: Int = 1,
resourceLoader: Font.ResourceLoader
): IntSize {
val paragraph = Paragraph(
paragraphIntrinsics = ParagraphIntrinsics(
text = text,
style = style,
density = density,
resourceLoader = resourceLoader
),
maxLines = maxLines,
ellipsis = false,
width = Float.POSITIVE_INFINITY
)
return IntSize(paragraph.minIntrinsicWidth.ceilToIntPx(), paragraph.height.ceilToIntPx())
}
// Source: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt#L47
internal const val DefaultWidthCharCount = 10
internal val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount)
// Needed because paragraph only calculates the height to display the text and not the entire height
// to display the decoration of the TextField Widget
internal val OutlinedTextBoxDecoration = 40.dp
// Source: https://github.com/androidx/androidx/blob/6075c715aea671a616890dd7f0fc9a50d96e75b9/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt#L296
internal fun Float.ceilToIntPx(): Int = ceil(this).roundToInt()
Additional discussion on this implementation and other options can be found here:
https://kotlinlang.slack.com/archives/CJLTWPH7S/p1621789835172600
Starting from M2 1.4.0-alpha02 and M3 1.1.0-alpha02 you can use the minLines attribute in the Text:
Text(
text = "MinLines = 3",
modifier = Modifier.fillMaxWidth().background(Yellow),
minLines = 3
)
Note that minLines is the minimum height in terms of minimum number of visible lines. It is required that 1 <= minLines <= maxLines.
You can use it with M2 and M3.
create custom Text
it doesn't work in #Preview but the runtime
#Composable
fun MinLineText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 0,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
var mText by remember { mutableStateOf(text) }
Text(
mText,
modifier,
color,
fontSize,
fontStyle,
fontWeight,
fontFamily,
letterSpacing,
textDecoration,
textAlign,
lineHeight,
overflow,
softWrap,
maxLines,
{
if (it.lineCount < minLines) {
mText = text + "\n".repeat(minLines - it.lineCount)
}
onTextLayout(it)
},
style,
)
}
usage
MinLineText(
text = "a sample text",
minLines = 2,
)

Categories

Resources