DEV Community

Cover image for A simple implementation of rounded background for text in Android
Konstantin
Konstantin

Posted on

A simple implementation of rounded background for text in Android

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:

Alt Text

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:

Alt 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:

Alt Text

Let's separate the drawing areas by color:

Alt Text

3. Multi-line, second-line longer. Looks like the previous case but use other fill-shapes:

Alt Text

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:

Alt Text

By combining these options, we can draw background for the text of any length:

Alt Text

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)