Search code examples
android-canvasspannablestring

Custom Span with path duplicates the path


I want to draw a curved underline for TextView Spans, so I have a custom span and textview as follows:

class UnderscoreSpan(context: Context) : ReplacementSpan() {

private val underscoreHeight = context.resources.getDimension(R.dimen.underscoreHeight)
private val vectorPath = Path()

override fun getSize(
    paint: Paint,
    text: CharSequence,
    start: Int,
    end: Int,
    fm: FontMetricsInt?
): Int {
    return measureText(paint, text, start, end).roundToInt()
}

private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float {
    return paint.measureText(text, start, end)
}

override fun draw(
    canvas: Canvas,
    text: CharSequence,
    start: Int,
    end: Int,
    x: Float,
    top: Int,
    y: Int,
    bottom: Int,
    paint: Paint
) {
    val w = measureText(paint, text, start, end)
    val h = bottom.toFloat() - top.toFloat()
    paint.color = Color.RED
    paint.strokeWidth = underscoreHeight
    paint.style = Paint.Style.STROKE
    vectorPath.moveTo(x, bottom.toFloat() - underscoreHeight)
    vectorPath.cubicTo(
        x + w / 2,
        bottom - underscoreHeight - h / 10,
        x + w - w / 5,
        bottom - underscoreHeight + h / 20,
        x + w,
        bottom - underscoreHeight,
    )
    canvas.save()
    canvas.drawPath(vectorPath, paint)
    paint.color = Color.BLACK
    paint.style = Paint.Style.FILL
    paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
    canvas.drawText(text, start, end, x, y.toFloat(), paint)
    canvas.restore()
}

}

class UnderscoreSpanTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

fun setSpannableText(text: String) {

    val start = text.findAnyOf(listOf("<b>"))?.first ?: 0
    val temp = text.replace("<b>", "")
    val end = temp.findAnyOf(listOf("</b>"))?.first ?: 0
    val finalText = temp.replace("</b>", "")

    val spannable = SpannableString(finalText)

    spannable.setSpan(
        UnderscoreSpan(context),
        start, end,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    super.setText(spannable)
}

}

However, the path is drawn twice with a few pixels offset. If I don't use a path (only drawRect for example), then it's fine. With the path however, it's always double, even if I close the path and don't use stroke. What am I doing wrong?


Solution

  • On the second onDraw call I failed to reset the path variable, so it was redrawn. So all it needed was a path.reset() call at the beginning of draw().