A simple implementation of rounded background for text in Android
Tags: android, ux, design, kotlin
Published: 2020-01-29 13:12:21

*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](https://dev-to-uploads.s3.amazonaws.com/i/468ki6x1fuiexrt8r7gh.png)

First I’ve checked [BackgroundColorSpan](https://developer.android.com/reference/android/text/style/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](https://developer.android.com/guide/topics/text/spans) is a method to style text at a character or paragraph level in Android Framework. I’ve created [RoundedBackgroundColorSpan](https://github.com/Semper-Viventem/RoundedBackgroundSpan/blob/master/app/src/main/java/ru/semper_viventem/roundedbackgroundspan/RoundedBackgroundColorSpan.kt) Kotlin class. It extends to [LineBackgroundSpan](https://developer.android.com/reference/android/text/style/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](https://dev-to-uploads.s3.amazonaws.com/i/7c6oqqvoqwq05g0v1mbs.jpg)

**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](https://dev-to-uploads.s3.amazonaws.com/i/66nb1vo79zwwqj4ioj87.jpg)

Let's separate the drawing areas by color:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/842botvohqbxd40ilxxu.jpg)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pjp0o15u9vqm0i5mi1y0.jpg)

**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](https://dev-to-uploads.s3.amazonaws.com/i/ijc818pmyjpc4zj7bzz9.jpg)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yp9dg86esp3c9qzusu47.jpg)

### 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`.

``` Kotlin
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`.

``` Kotlin
  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](https://github.com/Semper-Viventem/RoundedBackgroundSpan/blob/master/app/src/main/java/ru/semper_viventem/roundedbackgroundspan/RoundedBackgroundColorSpan.kt).

# 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.

``` Kotlin
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()) } }
    }
}
```