1

I'm trying to move a word above the next word in an android textview like in the attached image (example image). I have managed to shift the word upwards (like a superscript) with spannablestringbuilder, but I can't find a way to shift the right part of the text left in order to fill the gap. Does anyone have any idea how can this be done?

This is the function I've written so far:

/**
 * Adds clickable spans for words that are contained between "[" and "]"
 *
 * @param imString The string on which to apply clickable spans
 */
private fun addClickablePart(imString: String): SpannableStringBuilder
{
    var string = imString
    val spannableStringBuilder = SpannableStringBuilder((string.replace("[", "")).replace("]", ""))

    var startIndex = string.indexOf("[")

    while (startIndex != -1)
    {
        string = string.replaceFirst("[", "")
        val endIndex = string.indexOf("]", startIndex)
        string = string.replaceFirst("]", "")
        val clickString = string.substring(startIndex, endIndex)

        spannableStringBuilder.setSpan(
            object: ClickableSpan()
            {
                override fun onClick(view: View)
                {
                    HelperFunction.showToast(this@SongActivity, clickString)
                }

                override fun updateDrawState(text: TextPaint)
                {
                    super.updateDrawState(text)
                    text.isUnderlineText = false
                    text.color = ContextCompat.getColor(this@SongActivity, R.color.colorAccent)
                    text.textSize = HelperFunction.spToPx(this@SongActivity, 12).toFloat()
                    text.baselineShift += (text.ascent()).toInt() // move chord upwards
                    text.typeface = Typeface.create(ResourcesCompat.getFont(this@SongActivity, R.font.roboto_mono), Typeface.BOLD) // set text to bold
                }
            },
            startIndex, endIndex, 0)

        startIndex = string.indexOf("[", endIndex)
    }

    return spannableStringBuilder
}
Alex
  • 11
  • 3
  • Screenshot is showing what you have done or what you are expecting? – Vir Rajpurohit Jul 22 '19 at 08:15
  • 2
    Hello and welcome to SO, please read [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask) and provide come code. Otherwise your question will probably get downvoted and not answered. – Frieder Jul 22 '19 at 08:16
  • you should probably define this in html and then set textview contents to html – bvk256 Jul 22 '19 at 08:23
  • Hello! @VirRajpurohit the screenshot was showing what I was expecting. I provided the function I've written so far to process the string, and I updated the image to be more clear. – Alex Jul 22 '19 at 12:22
  • Hello @Frieder! I've provided the function I've written to process the string. – Alex Jul 22 '19 at 12:22
  • Hello @bvk256! Can you show me an example of how this can be done using html? I have added my function for processing the string and I updated the example image to clarify what I expect. – Alex Jul 22 '19 at 12:33

2 Answers2

0

You can use HTML in your TextView to achieve this, for example the HTML/CSS code for the superscript notation would be:

<!DOCTYPE html> 
<html> 
<head> 
<style> 
sup { 
    vertical-align: super; 
    font-size: medium; 
    color: red;
    position: relative; left: -2.5em; top: -0.5em; 
} 
</style> 
</head> 
<body> 
<p>word <sup>topword</sup></p> 
</body> 
</html> 

Please note that this is just an example and you need to fix the alignment.

To display it in a TextView use the following code:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    textView.setText(Html.fromHtml(htmlVal, Html.FROM_HTML_MODE_COMPACT));
} else { 
    textView.setText(Html.fromHtml(htmlVal));
}
bvk256
  • 1,837
  • 3
  • 20
  • 38
0

I've managed to solve the problem using ReplacementSpan. I'm posting the code below.

This is the custom ReplacementSpan class which draws the words on the canvas at the desired positions:

inner class ChordSpan: ReplacementSpan()
{
    override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: FontMetricsInt?): Int
    {
        val mText = text!!.subSequence(start, end).toString().replace("[", "")
        var chordString = ""
        var regularString = mText

        if (mText.contains("]"))
        {
            chordString = mText.substringBefore("]")
            regularString = mText.substringAfter("]")
        }

        val chordStringTextPaint = getChordStringTextPaint(paint)
        val regularStringTextPaint = getRegularStringTextPaint(paint)
        return max(chordStringTextPaint.measureText(chordString), regularStringTextPaint.measureText(regularString)).toInt()
    }

    private fun getChordStringTextPaint(paint: Paint): TextPaint
    {
        val textPaint = TextPaint(paint)
        textPaint.textSize = textPaint.textSize / 1.5F
        textPaint.typeface = Typeface.DEFAULT_BOLD
        textPaint.color = ContextCompat.getColor(this@SongActivity, R.color.colorAccent)
        return textPaint
    }

    private fun getRegularStringTextPaint(paint: Paint): TextPaint
    {
        return TextPaint(paint)
    }

    override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint)
    {
        val mText = text!!.subSequence(start, end).toString().replace("[", "")
        var chordString = ""
        var regularString = mText

        if (mText.contains("]"))
        {
            chordString = mText.substringBefore("]")
            regularString = mText.substringAfter("]")
        }

        val chordStringTextPaint = getChordStringTextPaint(paint)
        val regularStringTextPaint = getRegularStringTextPaint(paint)

        canvas.drawText(chordString, x, y.toFloat(), chordStringTextPaint)
        canvas.drawText(regularString, x, y.toFloat() + (bottom - top) / 2.5F, regularStringTextPaint)
    }
}

And this is the function that applies the spans on the hole text:

private fun formatDisplayOfLyricsWithChords(string: String): SpannableString
{
    val mString = "$string\n\n"
    val endOfStringIndex = mString.length
    val spannableString = SpannableString(mString)
    var startIndex = 0

    while (startIndex != -1 && startIndex != endOfStringIndex)
    {
        var possibleEndIndex = mString.indexOf("[", startIndex + 1)

        if (possibleEndIndex == -1)
        {
            possibleEndIndex = endOfStringIndex + 1
        }

        var endOfRowIndex = mString.indexOf("\n", startIndex + 1)

        if (endOfRowIndex == -1)
        {
            endOfRowIndex = endOfStringIndex + 1
        }

        val endIndex = minOf(possibleEndIndex, endOfRowIndex, endOfStringIndex)

        spannableString.setSpan(ChordSpan(), startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

        if (mString[startIndex] == '[')
        {
            val startIndexClick = startIndex
            val endIndexClick = mString.indexOf("]", startIndex + 1)
            val chord = mString.substring(startIndexClick + 1, endIndexClick)

            spannableString.setSpan(
                object: ClickableSpan()
                {
                    override fun onClick(view: View)
                    {
                        handleClickOnChord(chord)
                    }
                },
                startIndexClick, endIndexClick, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        }

        startIndex = endIndex

        if (startIndex == endOfRowIndex)
        {
            startIndex++
        }
    }

    return spannableString
}

I took inspiration from this answer from a similar question: https://stackoverflow.com/a/24091284/8211969

Alex
  • 11
  • 3