I know AutoSizing/Shrinking Text is on the Compose...
# compose
t
I know AutoSizing/Shrinking Text is on the Compose roadmap, but it's not clear when that will actually happen. I've seen some gists that iteratively make use of Text's
onTextLayout
closure. But I was asking myself, why not just use a Canvas rather than a Text if I'm just after simplified single line Text drawing, and want to specify a "minimum font size". Curious why this would be a bad idea? Or if I'm missing obvious things? Code on thread
Copy code
@Composable
fun Label(text: String, modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current, lowerSize: TextUnit = 10.sp) {
    val typeSetter = rememberTextMeasurer()
    var layout = typeSetter.measure(text = text, style = style, maxLines = 1)
    var (targetWidth, targetHeight) = with(LocalDensity.current) { layout.size.width.toDp() to layout.size.height.toDp() }
    Canvas(modifier = modifier.size(targetWidth, targetHeight)) {
       var layout = typeSetter.measure(text = text, style = style, maxLines = 1)
       if (layout.size.width > size.width) {
          // use minimum first it will tell us A) isn't going to fit even that small, and B give use a second point to interpolate with
          val smallestLayout = typeSetter.measure(text = text, style = style.copy(fontSize = lowerSize))
          layout = if (smallestLayout.size.width > size.width) {
             typeSetter.measure(
                text = text,
                style = style.copy(fontSize = lowerSize),
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                constraints = Constraints.fixedWidth(size.width.toInt())
             )
          } else {
             // good old y = mx + b, but (w)idth is x, and s(p) is y, 
             // so p = m*w + b, m = (p1 - p2) / (w1 - w2), and b = p - m*w
             val m = (style.fontSize.value - lowerSize.value) / (layout.size.width - smallestLayout.size.width).toFloat()
             val b = style.fontSize.value - (m * layout.size.width.toFloat())
             val p = (m * size.width.toFloat() + b).sp
             typeSetter.measure(text = text, style = style.copy(fontSize = p), maxLines = 1)
          }
       }
       val left = when(style.textAlign) {
          TextAlign.Center -> (size.width - layout.size.width) / 2
          TextAlign.Right, TextAlign.End -> size.width - layout.size.width
          else -> 0f
       }
       drawText(layout, color = style.color, topLeft = Offset(x = left, y = (size.height - layout.size.height) / 2))
    }
}
k
You’ll probably miss quite a few things, like accessibility mode “reading back” the text content, being able to select parts of the text, and others that come built in from the text composable
h
you also have the option to use
TextMeasurer
to do your binary search of the font size in a single layout phase then render your Text composable when the font size is ready.
t
I had to read that and ponder a few times. I think I get it now. You're suggesting a custom Layout where a side effect of the Layout is to accumulate the correct fontSize (and overflow value), and then have a Text inside that that that uses those values? I tried doing something like that (I think) by setting up a conversation between the onTextLayout and a drawWithContent, but that has issues, the "measured" intrinsice size of the layout is lost in the layout callback. I'm a little fuzzy on what you're suggesting @Halil Ozercan. Is it basically something like:
Copy code
fun MyLabel(text: String, modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current, lowerSize: TextUnit = 10.sp) {
    var bestStyle by remember(text, style) { mutableStateOf(style) }
    var bestOverflow by remember(text, style) { mutableStateOf(TextOverflow.Clip) }
    Layout(content = { 
        Text(text, modifier, bestStyle, bestOverflow)
     }, modifier = modifier) { m, c -> 
         ... SEARCH FOR BEST style AND OVERLFOW with a TEXT MEASURE here
         meausre and  place it as a pass through basically}
}
?
d
Basically call
TextMeasurer.measure()
until you get false for
hasVisualOverflow
. Assuming you pass the correct constraints it will work fine.
t
I did my best at what you suggested @Halil Ozercan, but I think my skills/understanding aren't quite there. It kinda works, but it flashes the "oversized" value each time it updates, and then gets the corrected style/fit
Copy code
@Composable
fun Label2(text: String, modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current, lowerSize: TextUnit = 10.sp) {
    var bestStyle by remember(text, style) { mutableStateOf(style) }
    var bestOverflow by remember(text, style) { mutableStateOf(TextOverflow.Clip) }

    val typeSetter = rememberTextMeasurer()
    val density = LocalDensity.current
    Layout(content = {
       Text(text = text, style = bestStyle, overflow = bestOverflow, softWrap = false, maxLines = 1)
    }, modifier = modifier) { measureables, constraints ->
       var fullParagraph = typeSetter.measure(text = text, style = style, maxLines = 1)
       if (fullParagraph.width > constraints.maxWidth) {
          // use minimum first it will tell us A) isn't going to fit even that small, and B) give use a second point to interpolate with
          val smallStyle = style.copy(fontSize = lowerSize)
          val minParagraph = typeSetter.measure(text = text, style = smallStyle, maxLines = 1)
          if (minParagraph.width > constraints.maxWidth) {
             bestStyle = smallStyle
             bestOverflow = TextOverflow.Ellipsis
          } else {
             // good old y = mx + b, but (w)idth is x, and s(p) is y, 
             // so p = m*w + b, m = (p1 - p2) / (w1 - w2), and b = p - m*w
             val m = (style.fontSize.value - lowerSize.value) / (fullParagraph.width - minParagraph.width).toFloat()
             val b = style.fontSize.value - (m * fullParagraph.width.toFloat())
             val p = (m * constraints.maxWidth.toFloat() + b).sp
             p.logged("smallSize")
             bestStyle = style.copy(fontSize = p)
          }
       }
       val placeble = measureables.first().measure(constraints.copy(minWidth = 0, minHeight = 0))
       layout(constraints.maxWidth, with(density) { bestStyle.lineHeight.roundToPx() }) {
          placeble.place(0, 0)
       }
    }
}
k
Would that be different from
BoxWithConstraints
or a vanilla
SubcomposeLayout
, @Halil Ozercan? Sounds like this needs to have two passes, one to determine the size of the text, and the other to add the configured
Text
composable as a child.
d
That's pretty much what I do, you can have a function that takes gets passed the usual
Text
parameters that you need +
Constraints
and have it return a new
TextStyle
that fits the constraints. Then you move the problem to how to pass the correct constraints, which I've done via
BoxWithConstraints
h
Sorry for being vague with my answer but I agree with the rest of the discussion that happened here. You can't really do AutosizeText using the Text composable without either missing a frame or using
SubcomposeLayout/BoxWithConstraints
.
t
So, is this really one of those cases where we get to use BoxWithConstraints, and not feel that we're sinning before the Compose gods? Seems every time, BoxWithConstraints comes up here, there's a loud hue and cry and warning that You'reHoldingItWrong(tm).
h
So the thing about BoxWithConstraints/SubcomposeLayout is that it's very rare that you'd change something in Composition according to your layout requirements. Layout phase should be self-sufficient to achieve 99% of your layout requirements if those requirements were well defined in Composition. Large Screen support is one of those that truly need Subcomposition since you'd want to know how much you can fit on the screen to decide what you need to compose. In this situation, ideally you wouldn't need subcomposition. If Text supported a range of values for the fontSize in TextStyle,
Text
could figure out the correct font size during layout, then draw the calculated TextLayoutResult in a single frame. Unfortunately
Text
only accepts a single fontSize during composition. Since you have to figure out the right fontSize before composition ends, you also need to know your layout constraints, hence the need for
BoxWithConstraints
.
d
Thanks for the nice explanation Halil. One quick question - are usages of
BoxWithConstraints
less impactful for performance if it's at a very leaf node? i.e. in this case, out of the whole layout only some inner most part (
Text
) needs to go in it, is that less of a sin compared to using
BoxWithConstraints
at some top level?
☝️ 1
h
I don't consider myself a Compose cleric so I wouldn't know if it's a sin. @shikasd any comments?
s
BoxWithConstraints
creates a composition regardless of its place in hierarchy. Consider using Modifier.layout instead, if possible
To be fair, it should be fine to have it one or two places, just try to avoid it for e.g. every item in lazy list
d
But is there a way to make this functionality mentioned (shrinking text) work with Modifier.layout at all? Maybe I'm missing something...
s
Not sure, it sounds like fontSize is only configured through composition, so probably not possible with the current APIs
1
c
can't wait for autosizetext to be natively available 😅
158 Views