Is it possible to make a growing Text Composable physically scrollable when the text reaches a specific length instead of this happening only when it reaches its container bounds? I want to allow the user to physically scroll the Text Composable above a specific text length before it reaches its container bounds.
Current Composable
val scrollState = rememberScrollState(0)
Text(
text = growingText,
modifier = Modifier.horizontalScroll(scrollState),
color = Color.Gray
)
You have to make sure your text content is big enough to enable the scroll and to do it you have to change the width according to the lenght.
Something like:
val maxCount = 6
var textWidth by remember { mutableStateOf<Int?>(null) }
val widthModifier = textWidth?.let { width ->
with(LocalDensity.current) {
Modifier.width( if (text.length >= maxCount)
(width-30).toDp() //reduce the width to enable the scroll
else
width.toDp())
}
} ?: Modifier
val scrollState = rememberScrollState()
Text(
text = text,
modifier = Modifier
.then(widthModifier)
.horizontalScroll(state = scrollState),
color = Color.Gray,
onTextLayout = { textWidth = it.size.width }
)
Related
I want to removed side padding of particular child item in LazyColum. I solved this problem in xml with the help of this post. I have same scenario in the jetpack compose. I am using BOM versions of compose_bom = "2022.11.00" with Material 3.
Card(shape = RoundedCornerShape(6.dp),) {
Column(modifier.background(Color.White)) {
LazyColumn(
contentPadding = PaddingValues(all =16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
item {
Text(
text = "Device Header",
modifier = Modifier.padding(top = 10.dp),
style = headerTextStyle
)
}
item {
Divider() // remove padding from side in here
}
}
}
}
Actual Output
Expected Output
In Compose you can't use a negative padding in the children to reduce the padding applied by the parent container. You can use offset modifer with a negative value but it will shift the Divider on the left side.
You can use a layout modifier to apply an horizontal offset increasing the width.
Something like:
LazyColumn(
Modifier.background(Yellow),
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
//...
item {
val sidePadding = (-8).dp
Divider(modifier = Modifier
.layout { measurable, constraints ->
// Measure the composable adding the side padding*2 (left+right)
val placeable =
measurable.measure(constraints.offset(horizontal = -sidePadding.roundToPx() * 2))
//increase the width adding the side padding*2
layout(
placeable.width + sidePadding.roundToPx() * 2,
placeable.height
) {
// Where the composable gets placed
placeable.place(+sidePadding.roundToPx(), 0)
}
}
)
}
}
Here you can find the output with a Divider() without modifier, and the Divider with the layout modifier.
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.
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
}
}
)
As can be seen in official documents there is layout named SubcomposeLayout defined as
Analogue of Layout which allows to subcompose the actual content
during the measuring stage for example to use the values calculated
during the measurement as params for the composition of the children.
Possible use cases:
You need to know the constraints passed by the parent during the
composition and can't solve your use case with just custom Layout or
LayoutModifier. See
androidx.compose.foundation.layout.BoxWithConstraints.
You want to use the size of one child during the composition of the
second child.
You want to compose your items lazily based on the available size. For
example you have a list of 100 items and instead of composing all of
them you only compose the ones which are currently visible(say 5 of
them) and compose next items when the component is scrolled.
I searched Stackoverflow with SubcomposeLayout keyword but couldn't find anything about it, created this sample code, copied most of it from official document, to test and learn how it works
#Composable
private fun SampleContent() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SubComponent(
mainContent = {
Text(
"MainContent",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
dependentContent = {
val size = it
println("🤔 Dependent size: $size")
Column() {
Text(
"Dependent Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
}
)
}
}
#Composable
private fun SubComponent(
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize) -> Unit
) {
SubcomposeLayout { constraints ->
val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
layout(maxSize.width, maxSize.height) {
mainPlaceables.forEach { it.placeRelative(0, 0) }
subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize)
}.forEach {
it.measure(constraints).placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
It's supposed to re-measure a component based on another component size but what this code actually does is a mystery to me.
How does subcompose function work?
What's the point of slotId and can we get slotId in a way?
The description for subCompose function
Performs subcomposition of the provided content with given slotId.
Params: slotId - unique id which represents the slot we are composing
into. If you have fixed amount or slots you can use enums as slot ids,
or if you have a list of items maybe an index in the list or some
other unique key can work. To be able to correctly match the content
between remeasures you should provide the object which is equals to
the one you used during the previous measuring. content - the
composable content which defines the slot. It could emit multiple
layouts, in this case the returned list of Measurables will have
multiple elements.
Can someone explain what it means or/and provide a working sample for SubcomposeLayout?
It's supposed to re-measure a component based on another component size...
SubcomposeLayout doesn't remeasure. It allows deferring the composition and measure of content until its constraints from its parent are known and some its content can be measured, the results from which and can be passed as a parameter to the deferred content. The above example calculates the maximum size of the content generated by mainContent and passes it as a parameter to deferredContent. It then measures deferredContent and places both mainContent and deferredContent on top of each other.
The simplest example of how to use SubcomposeLayout is BoxWithConstraints that just passes the constraints it receives from its parent directly to its content. The constraints of the box are not known until the siblings of the box have been measured by the parent which occurs during layout so the composition of content is deferred until layout.
Similarly, for the example above, the maxSize of mainContent is not known until layout so deferredContent is called in layout once maxSize is calculated. It always places deferredContent on top of mainContent so it is assumed that deferredContent uses maxSize in some way to avoid obscuring the content generated by mainContent. Probably not the best design for a composable but the composable was intended to be illustrative not useful itself.
Note that subcompose can be called multiple times in the layout block. This is, for example, what happens in LazyRow. The slotId allows SubcomposeLayout to track and manage the compositions created by calling subcompose. For example, if you are generating the content from an array you might want use the index of the array as its slotId allowing SubcomposeLayout to determine which subcompose generated last time should be used to during recomposition. Also, if a slotid is not used any more, SubcomposeLayout will dispose its corresponding composition.
As for where the slotId goes, that is up to the caller of SubcomposeLayout. If the content needs it, pass it as a parameter. The above example doesn't need it as the slotId is always the same for deferredContent so it doesn't need to go anywhere.
I made a sample based on the sample provided by official documents and #chuckj's answer but still not sure if this efficient or right way to implement it.
It basically measures longest component sets parent width and remeasures shorter one with minimumWidth of Constraint and resizes short one as can be seen in this gif. This is how whatsapp scales quote and message length basically.
Orange and pink containers are Columns, which direct children of DynamicWidthLayout, that uses SubcomposeLayout to remeasure.
#Composable
private fun DynamicWidthLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
var maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) {
// 🔥🔥 Send maxSize of mainComponent to
// dependent composable in case it might be used
dependentContent(maxSize)
}
val dependentPlaceables: List<Placeable> = dependentMeasurables
.map { measurable: Measurable ->
measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
}
// Get maximum width of dependent composable
val maxWidth = dependentPlaceables.maxOf { it.width }
println("🔥 DynamicWidthLayout-> maxSize width: ${maxSize.width}, height: ${maxSize.height}")
// If width of dependent composable is longer than main one, remeasure main one
// with dependent composable's width using it as minimumWidthConstraint
if (maxWidth > maxSize.width) {
println("🚀 DynamicWidthLayout REMEASURE MAIN COMPONENT")
// !!! 🔥🤔 CANNOT use SlotsEnum.Main here why?
mainPlaceables = subcompose(2, mainContent).map {
it.measure(Constraints(maxWidth, constraints.maxWidth))
}
}
// Our final maxSize is longest width and total height of main and dependent composables
maxSize = IntSize(
maxSize.width.coerceAtLeast(maxWidth),
maxSize.height + dependentPlaceables.maxOf { it.height }
)
layout(maxSize.width, maxSize.height) {
// Place layouts
mainPlaceables.forEach { it.placeRelative(0, 0) }
dependentPlaceables.forEach {
it.placeRelative(0, mainPlaceables.maxOf { it.height })
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
#Composable
private fun TutorialContent() {
val density = LocalDensity.current.density
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
var mainText by remember { mutableStateOf(TextFieldValue("Main Component")) }
var dependentText by remember { mutableStateOf(TextFieldValue("Dependent Component")) }
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = mainText,
label = { Text("Main") },
placeholder = { Text("Set text to change main width") },
onValueChange = { newValue: TextFieldValue ->
mainText = newValue
}
)
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = dependentText,
label = { Text("Dependent") },
placeholder = { Text("Set text to change dependent width") },
onValueChange = { newValue ->
dependentText = newValue
}
)
DynamicWidthLayout(
modifier = Modifier
.padding(8.dp)
.background(Color.LightGray)
.padding(8.dp),
mainContent = {
println("🍏 DynamicWidthLayout-> MainContent {} composed")
Column(
modifier = Modifier
.background(orange400)
.padding(4.dp)
) {
Text(
text = mainText.text,
modifier = Modifier
.background(blue400)
.height(40.dp),
color = Color.White
)
}
},
dependentContent = { size: IntSize ->
// 🔥 Measure max width of main component in dp retrieved
// by subCompose of dependent component from IntSize
val maxWidth = with(density) {
size.width / this
}.dp
println(
"🍎 DynamicWidthLayout-> DependentContent composed " +
"Dependent size: $size, "
+ "maxWidth: $maxWidth"
)
Column(
modifier = Modifier
.background(pink400)
.padding(4.dp)
) {
Text(
text = dependentText.text,
modifier = Modifier
.background(green400),
color = Color.White
)
}
}
)
}
}
And full source code is here.
Recently i needed to use almost the same SubcomposeLayout in question. I needed a Slider with a Composable thumb that i needed to get its width so i can set start and end of track and full width of Slider i was getting from BoxWithConstraints.
enum class SlotsEnum {
Slider, Thumb
}
/**
* [SubcomposeLayout] that measure [thumb] size to set Slider's track start and track width.
* #param thumb thumb Composable
* #param slider Slider composable that contains **thumb** and **track** of this Slider.
*/
#Composable
private fun SliderComposeLayout(
modifier: Modifier = Modifier,
thumb: #Composable () -> Unit,
slider: #Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val thumbPlaceable: Placeable = subcompose(SlotsEnum.Thumb, thumb).map {
it.measure(constraints)
}.first()
// Width and height of the thumb Composable
val thumbSize = IntSize(thumbPlaceable.width, thumbPlaceable.height)
// Whole Slider Composable
val sliderPlaceable: Placeable = subcompose(SlotsEnum.Slider) {
slider(thumbSize, constraints)
}.map {
it.measure(constraints)
}.first()
val sliderWidth = sliderPlaceable.width
val sliderHeight = sliderPlaceable.height
layout(sliderWidth, sliderHeight) {
sliderPlaceable.placeRelative(0, 0)
}
}
}
Measured thumb and send its dimensions as IntSize and Constraints to Slider, and only placed Slider since thumb is already placed insider Slider, placing here creates two thumbs.
And used it as
SliderComposeLayout(
modifier = modifier
.minimumTouchTargetSize()
.requiredSizeIn(
minWidth = ThumbRadius * 2,
minHeight = ThumbRadius * 2,
),
thumb = { thumb() }
) { thumbSize: IntSize, constraints: Constraints ->
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val width = constraints.maxWidth.toFloat()
val thumbRadiusInPx = (thumbSize.width / 2).toFloat()
// Start of the track used for measuring progress,
// it's line + radius of cap which is half of height of track
// to draw this on canvas starting point of line
// should be at trackStart + trackHeightInPx / 2 while drawing
val trackStart: Float
// End of the track that is used for measuring progress
val trackEnd: Float
val strokeRadius: Float
with(LocalDensity.current) {
strokeRadius = trackHeight.toPx() / 2
trackStart = thumbRadiusInPx.coerceAtLeast(strokeRadius)
trackEnd = width - trackStart
}
// Rest of the code
}
Result
Github link for the code
I want to display a dynamic multiple lines text and an icon at the end of the last line. This icon can be animate. I try some ways but not success yet. How should I do?
Example view which had the same idea with my layout
In the Text composable you can use the inlineContent to define a map of tags that replaces certain ranges of the text. It's used to insert composables into text layout.
Then using a Placeholder you can reserve space in text layout.
Something like:
val myId = "inlineContent"
val text = buildAnnotatedString {
append("Where do you like to go?")
// Append a placeholder string "[icon]" and attach an annotation "inlineContent" on it.
appendInlineContent(myId, "[icon]")
}
val inlineContent = mapOf(
Pair(
// This tells the [CoreText] to replace the placeholder string "[icon]" by
// the composable given in the [InlineTextContent] object.
myId,
InlineTextContent(
// Placeholder tells text layout the expected size and vertical alignment of
// children composable.
Placeholder(
width = 12.sp,
height = 12.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.AboveBaseline
)
) {
// This Icon will fill maximum size, which is specified by the [Placeholder]
// above. Notice the width and height in [Placeholder] are specified in TextUnit,
// and are converted into pixel by text layout.
Icon(Icons.Filled.Face,"",tint = Color.Red)
}
)
)
Text(text = text,
modifier = Modifier.width(100.dp),
inlineContent = inlineContent)
It is a composable so you can use your favorite animation.
Just an example:
var blue by remember { mutableStateOf(false) }
val color by animateColorAsState(if (blue) Blue else Red,
animationSpec = tween(
durationMillis = 3000
))
and change the Icon to
Icon(Icons.Filled.Face,"", tint = color)