Friday, 30 November 2018

Multiline `ReplacementSpan` drawing issue

My custom replacement span works as long as text is not too long but as soon as text is longer than one line, span drawing completely breaks apart. My understanding is that draw() gets called twice in this case causing span to draw twice. There is no way to differentiate that second draw call from first one, giving you control over what to draw and where. start and end become useless as they report wrong values.

Is ReplacementSpan supposed to even work for multiline text? I would appreciate any help to resolve this issue.

enter image description here

This is what happens when I change selected text to my CustomReplacementSpan:

enter image description here

CustomReplacementSpan.kt

import android.graphics.Canvas
import android.graphics.Paint
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.text.style.ReplacementSpan
import androidx.core.graphics.withTranslation

class CustomReplacementSpan(val spanText: String, val color: Int) : ReplacementSpan() {

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

    override fun draw(
        canvas: Canvas,
        text: CharSequence?,
        start: Int,
        end: Int,
        x: Float,
        top: Int,
        y: Int,
        bottom: Int,
        paint: Paint
    ) {
        paint.color = color

        canvas.drawMultilineText(
            text = spanText,
            textPaint = paint as TextPaint,
            width = canvas.width,
            x = x,
            y = top.toFloat()
        )
    }


}

fun Canvas.drawMultilineText(
    text: CharSequence,
    textPaint: TextPaint,
    width: Int,
    x: Float,
    y: Float,
    start: Int = 0,
    end: Int = text.length,
    alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
    spacingMult: Float = 1f,
    spacingAdd: Float = 0f,
    includePad: Boolean = true,
    ellipsizedWidth: Int = width,
    ellipsize: TextUtils.TruncateAt? = null
) {
    val staticLayout =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            StaticLayout.Builder.obtain(text, start, end, textPaint, width)
                .setAlignment(alignment)
                .setLineSpacing(spacingAdd, spacingMult)
                .setIncludePad(includePad)
                .setEllipsizedWidth(ellipsizedWidth)
                .setEllipsize(ellipsize)
                .build()
        } else {
            StaticLayout(
                text, start, end, textPaint, width, alignment,
                spacingMult, spacingAdd, includePad, ellipsize, ellipsizedWidth
            )
        }

    staticLayout.draw(this, x, y)
}

private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
    canvas.withTranslation(x, y) {
        draw(this)
    }
}

MainActivity.kt

import android.os.Bundle
import android.text.Spannable
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun applySpan(view: View) {
        val editText = findViewById<EditText>(R.id.edit)
        if (editText.selectionStart < 0 || editText.selectionEnd < 0) {
            return
        }
        val fullText = editText.text
        val text = fullText.subSequence(editText.selectionStart, editText.selectionEnd)
        val span = CustomReplacementSpan(text.toString(), ContextCompat.getColor(this, android.R.color.holo_blue_dark))
        editText.text.setSpan(span, editText.selectionStart, editText.selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit"
        style="@style/Widget.AppCompat.EditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="applySpan"
        android:text="Make it span" />

</LinearLayout>



from Multiline `ReplacementSpan` drawing issue

No comments:

Post a Comment