It’s a simple implementation of text background with rounded corners for Android.
Problem
I had a need to implement colored highlighting of text with rounded corners. Like this:
First I’ve checked BackgroundColorSpan, but it’s doesn’t solve the problem with rounded corners. And I wanted to implement my decision. It’s a simple method for drawing the text’s background with a rounded corner.
Implementation
Span is a method to style text at a character or paragraph level in Android Framework. I’ve created RoundedBackgroundColorSpan Kotlin class. It extends to LineBackgroundSpan because LineBackgroundSpan can draw decoration-layer for text line by line.
let’s see what’s cases we have:
1. One-line text. It’s the most simple case. We will just draw a rectangle with rounded corners around the text:
2. Multi-line text, first-line longer. In this case, we draw a rounded rectangle for every line. And draw rounded shapes to fill the corners:
Let's separate the drawing areas by color:
3. Multi-line, second-line longer. Looks like the previous case but use other fill-shapes:
4. Multi-line, lines of the same width. If second-line more short that first line but the difference in width is less than the radius, use the previous line’s width for next line:
By combining these options, we can draw background for the text of any length:
What about code?
For LineBackgroundSpan we must implement the method drawBackground(...). This method executes for every line drawing. In this method, we will draw rectangles (RectF) for every line on Canvas.
override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lineNumber: Int
) {
val actualWidth = p.measureText(text, start, end) + 2f * padding
val widthDiff = abs(prevWidth - actualWidth)
val diffIsShort = widthDiff < 2f * radius
val width = if (lineNumber == 0) {
actualWidth
} else if ((actualWidth < prevWidth) && diffIsShort) {
prevWidth
} else if ((actualWidth > prevWidth) && diffIsShort) {
actualWidth + (2f * radius - widthDiff)
} else {
actualWidth
}
val shiftLeft = 0f - padding
val shiftRight = width + shiftLeft
rect.set(shiftLeft, top.toFloat(), shiftRight, bottom.toFloat())
c.drawRoundRect(rect, radius, radius, paint)
if (lineNumber > 0) {
drawLeftFillShape(c, rect, radius)
when {
prevWidth < width -> drawBottomFillShape(c, rect, radius)
prevWidth > width -> drawTopFillShape(c, rect, radius)
else -> drawRightFillShape(c, rect, radius)
}
}
prevWidth = width
prevRight = rect.right
}
And if the line’s index is not zero, we must draw decoration shapes. Just draw it using Path.
private fun drawLeftCorner(c: Canvas, rect: RectF, radius: Float) {
path.reset()
path.moveTo(rect.left, rect.top + radius)
path.lineTo(rect.left, rect.top - radius)
path.lineTo(rect.left + radius, rect.top)
path.lineTo(rect.left, rect.top + radius)
c.drawPath(path, paint)
}
private fun drawTopCorner(c: Canvas, rect: RectF, radius: Float) {
path.reset()
path.moveTo(prevRight + radius, rect.top)
path.lineTo(prevRight - radius, rect.top)
path.lineTo(prevRight, rect.top - radius)
path.cubicTo(
prevRight, rect.top - radius,
prevRight, rect.top,
prevRight + radius, rect.top
)
c.drawPath(path, paint)
}
private fun drawBottomCorner(c: Canvas, rect: RectF, radius: Float) {
path.reset()
path.moveTo(rect.right + radius, rect.top)
path.lineTo(rect.right - radius, rect.top)
path.lineTo(rect.right, rect.top + radius)
path.cubicTo(
rect.right, rect.top + radius,
rect.right, rect.top,
rect.right + radius, rect.top
)
c.drawPath(path, paint)
}
private fun drawRightCorner(c: Canvas, rect: RectF, radius: Float) {
path.reset()
path.moveTo(rect.right, rect.top - radius)
path.lineTo(rect.right, rect.top + radius)
path.lineTo(rect.right - radius, rect.top)
path.lineTo(rect.right, rect.top - radius)
c.drawPath(path, paint)
}
The full class you can find here.
Usage
You can look at implementation on GitHub: https://github.com/Semper-Viventem/RoundedBackgroundSpan. Now it does not work for some cases (ex. center and right text formatting). But maybe my solution is right for your case.
private fun initSpannableText() {
val span = RoundedBackgroundColorSpan(
backgroundColor = colors.random(),
padding = dp(5),
radius = dp(5)
)
with(spanText) {
setShadowLayer(dp(10), 0f, 0f, 0) // it's important for padding working
text = androidx.core.text.buildSpannedString { inSpans(span) { append(text.toString()) } }
}
}







Top comments (0)