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)