This week at Betclic, I had to implement a very simple layout as you can see:
Nop sorry, that’s not the real app screen ;)
Ok, what’s our business constraints here?
As you may already know, Android offers two pieces made for this kind of stuff:
This should be EASY! Right?
I start to create a ConstraintLayout with 3 TextViews. The 1st TextView is constrained to parent on start/top/bottom with some margins, the 2 other views are topToBottom of the 1st and 2nd view as you can expect.
As it’s fully constrained horizontally, it’s recommended to use
layout_width="0dp" and since I want the 3 texts to be regrouped on the vertical axis, I started with
layout_height="wrap_content". Finally I enabled autosizing with
app:autoSizeTextType="uniform", let’s see the beautiful result!
Did I miss something? Oh yes, the autosizing takes all the place available on BOTH axes. Here it has more space on the horizontal axis, but on the vertical axis it’s limited by our
May produce unexpected results? No, it certainly produces unexpected results :)
Makes sense but not helpful, I wish we could select a type
horizontal instead of
So if I want to autosize a line, I need to provide the height… but how? Since my content is dynamic, did I need to compute the height myself? And if I need to compute it, why the point to use autosize at all?
Well ok, let’s forget the height for now and include the picture with ImageSpan, so we have all elements for the compute.
ImageSpan constructor offers a 2nd parameter to align your ImageSpan, but it aligns the bottom of the picture with the bottom or the baseline of the font. Here I want to align on the baseline but also align the top of the picture with the ascent. (If you want a nice explanation about baseline/ascent/descent.)
Custom fonts allows you to add vector icons from a unicode. to Yes the need is very similar to emojis so we could have created a custom font, it could have matched our needs pretty easily, but it has also some drawbacks:
But if you don’t care about these drawbacks or have straegies to mitigate them, I heavily recommend to use a custom font.
At this time I thought it should not be that difficult to compute a scale ratio manually and apply it to the text and the picture.
textView.paint.getTextBounds("mytxt", 0, 5, bounds)to extract the width of the text
TextView.widthto determine the available space
textSize=testSize * (textViewWidth / textBoundsWidthFloat)
The ratio is the scale to apply to the text size so the next call to getTextBounds should return the width of the TextView…
Please don’t do that
We’ve pushed the idea to compute the TextView height, get an almost stable result, but in some cases with our different fonts, there were errors due to floating compute precision, font inter-char spacing. Even with a magic const 0.97 to hide errors, we discovered on low-end devices that the layout was broken (text on 2 lines, too big margins, words disappearing).
Also the other limitations of this almost-ok approach :
Some things to know about this class:
StaticLayoutand call the measurement on it. No render needed so no artifact or big impact on perf.
I dislike reflection code, it’s so fragile that you can break your layout when updating the lib without anyone noticing the problem, until it’s in production. Especially for UI, the behavior can slightly change, crop a text, and most UI tests will still be green…
Anyway, too much time spent to align 3 texts and an image, we need a solution, so go for Reflection as the support library does the same.
A simple copy paste of the 2 methods and I’m able to get the available size computed by the autosize mechanism.
Now that we have the computed value, let’s see the interesting part, the
suggestedSizeFitsInSpace that we need to modify for our needs
So 2 problems here:
For the 1st problem, an easy solution is to create another method
suggestedSizeFitsInWidth that call the above method with
RectF(0,0,availableWidth, 1024*1024). It simulates I’ve a super long height to do the compute, so the final condition is skipped. Why
1024*1024? It’s ported from
TextView#VERY_WIDE that represents a maximum width in pixels the TextView takes when horizontal scrolling is activated.
Advantage of this approach, no code modification of the original method, so I know it’s supposed to work.
For the 2nd problem, I need to update the original code, so just after the
setTextSize(suggestedSizeInPx) I add this line of code:
I copy pasted the binary search of the AppCompat library, since we add an additional parameter the underneath method wasn’t easily re-usable.
So now we can get the autoSize XML values from the AppCompatTextView to run our binary search but we’ve to take somethings into account here.
You’ve to define the autosize type to uniform if you want the AppCompatTextView generate the available sizes array. This array is used by the binary search to find the best font size.
When we use the setTextSize method, the parameters are ignored if the autosize is running.
So we need to enable > compute > disable > setSize, and if the method is called twice, since the disable clean the XML values, we need to store them somewhere…
That’s unfortunate as we’ve moved to an extension function implementation, now we need to store value to the TextView, and we cannot add/store value via Reflection, and I don’t like overriding TextView as it’s quickly not scalable. So here is a quick hack: we use a WeakReference on the view itself, and then compare the view to restore the previous values.
Ok so let’s have a look at the final extension function now:
As you can notice, we’re clearly not done yet, the custom font is not loaded (but there is enough tutorial about that), there is too much space between lines (next article maybe?)… BUT for this first article, the left and right are aligned, the picto is adjusted, and the final implementation re-use autosize parameters and doesn’t consume many more resources than a standard autosize. You can grab the full code from this sandbox project: https://github.com/glureau/atvasis