I'm using a custom replacement span to draw a background around particular words (adds padding around the words, words are identified by surrounding symbols, much like an HTML tag), and to even change the text size of those words (although I'm not doing so in this case). This works just fine when there is a single line, everything shows as expected:
This is done by adjusting the FontMetrics in the overridden getSize
, adjusting top and bottom to add the padding for background, and adjusting the returned size (width) to add the same, like this:
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
if (textSize != null) {
paint.textSize = textSize
}
if (fm != null) {
val newMetrics = paint.fontMetricsInt
fm.descent = newMetrics.descent
fm.ascent = newMetrics.ascent
fm.leading = newMetrics.leading
fm.top = (newMetrics.top - strokeWidth - padding).roundToInt()
fm.bottom = (newMetrics.bottom + strokeWidth + padding).roundToInt()
}
return (padding + strokeWidth + paint.measureText(
text.subSequence(start + 1, end - 1).toString().uppercase()
) + padding + strokeWidth).roundToInt()
}
The onDraw
handles drawing the rectangles and the text to the canvas.
The problem comes with multiline text. I know that the contents of a ReplacementSpan won't wrap/split, the entire ReplacementSpan will be wrapped, and that's expected and wanted in this case. The problem appears to be with positioning of text within the chips when we go to multiline. I get some odd values for top/bottom, that have different sizes than expected based on the font metrics from getSize(). On the first line, the text shows at the bottom of the chip, and the second line the text shows at the top of the chip:
This looks to me like the line height is not being adjusted to handle the additional padding when there are multiple lines. I've tried implementing LineHeightSpan in my ReplacementSpan, but that doesn't work, because it must be applied to the entire paragraph.
The closest I've come to getting it to work, is to apply a LineHeightSpan, using the explicit height:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mySpan.setSpan(LineHeightSpan.Standard(71),0,narrativeString.length-1,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
This isn't a real solution though, because it doesn't adjust based on the height of the ReplacementSpan. And it appears to be just a little off (boxes appear to be a little bit taller than they should be, and text appears to be a little bit towards the bottom, instead of centered):
Is there any way to get this to work properly, apart from creating a separate view for each word, and plugging them into something like a FlexboxLayout?
Update: I've attempted the method suggested by Zain, following the article and repo suggested by Zain, and it doesn't work either.
Firstly, the horizontal padding doesn't impact the width of the "word". If you put a chip on two adjacent words, the padding will overlap. Secondly, the vertical padding doesn't actually change the line height. Adding vertical padding will overlap the background onto other lines if the padding goes beyond the line height, or chips that are on adjacent line. Any padding that falls outside the text view (e.g., above first line, below last line), gets cut off.
The
getLineForOffset()
can be used to detect the multi-lined text of a span:And each case can be handled with a unique renderer before drawing to the canvas. This allows to handle the first line of text, middle lines, and last line of text with different drawables so that the spanned area seems to be coherent across the lines:
This is well-handled by this repo which also puts LTR/RTL text directions into consideration:
The repo applies this to a custom
TextView
:And the desired span drawable can be attached in XML with additional attributes defined in TextRoundedBgAttributeReader.
Sample usage:
And you can either annotate spans either in strings.xml:
Or programmatically:
This article explains that deeply; and the mentioned repo has samples of testing.