Kotlin set two different text styles to dynamically changing text in a text view using SpannableString

8k views Asked by At

I have a text view with a value that can change. For example, it's Pressure. It can have different length - i.e. 999.1 or 1010.2 hPa.

The text looks like this:

enter image description here

I need to set different text styles for the first part of the text (the number - 1024.12) and the second part of the text (the unit of measurement).

So far I have tried:

  1. Using SpannableString and set spans to the same scannable string.

  2. Creating two scannable strings, setting spans on them separately, joining two scannable strings and setting that joined scannable strings as text to text view:

//the value of measurement
val spannable1 = SpannableString(button.customView.tvBottom.text.split(" ")[0]) 
//the unit of measurement
val spannable2 = SpannableString(button.customView.tvBottom.text.split(" ")[1]) 
                
spannable1.setSpan(R.style. Bold20Dark, 0, spannable1.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable2.setSpan(R.style. Regular12Dark, 0, spannable2.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                
val spannableString = spannable1.toString() + spannable2.toString()
button.customView.tvBottom.setText(spannableString)

What stops me is the dynamic change of the number and its length, so I cannot figure out how to set a stable range if it constantly changes.

By different text styles I mean following styles:

 <style name="Bold20Dark">
        <item name="android:textSize">20sp</item>
        <item name="android:lineSpacingExtra">3sp</item>
        <item name="android:textColor">@color/dark</item>
        <item name="android:fontFamily">@font/fonr_bold</item>
    </style>

    <style name="Regular12Dark">
        <item name="android:gravity">center_horizontal</item>
        <item name="android:textSize">12sp</item>
        <item name="android:lineSpacingExtra">4sp</item>
        <item name="android:fontFamily">@font/font_regular</item>
        <item name="android:textColor">@color/grey</item>
    </style>

Is there a way to set two text styles if the range is changeable? Thank you.

5

There are 5 answers

0
Gregor Rant On BEST ANSWER

You can make a spannable factory, override the newSpannable method and set it to the TextView. You just have to make sure you call the setText as setText(string, BufferType.SPANNABLE)

val spannableFactory = object : Spannable.Factory() {
    override fun newSpannable(source: CharSequence?): Spannable {
        val spannable = source!!.toSpannable()
        val len1 = source.split(" ")[0].length
        val len2 = source.split(" ")[1].length

        spannable.setSpan(ForegroundColorSpan(Color.RED), 0, len1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        spannable.setSpan(ForegroundColorSpan(Color.BLUE), len1, len1+len2+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        return spannable
    }
}
button.customView.tvBottom.setSpannableFactory(spannableFactory)
button.customView.tvBottom.setText("1010.2 hPa", TextView.BufferType.SPANNABLE)
1
Fuad Saneen On

This may help you..

TextView txtView = (TextView) findViewById(R.id.text);
        String number ="1024.12";
        String unit = " hPa";

        SpannableString spannableString = new SpannableString(number+unit);
        spannableString.setSpan( new BottomAlignSuperscriptSpan( (float)0.35 ), number.length(), number.length()+unit.length(),
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
        txtView.setText(spannableString);

BottomAlignSuperscriptSpan.java

import android.text.TextPaint;
import android.text.style.SuperscriptSpan;

public class BottomAlignSuperscriptSpan extends SuperscriptSpan {

    //divide superscript by this number
    protected int fontScale = 2;
    //shift value, 0 to 1.0
    protected float shiftPercentage = 0;
    //doesn't shift
    BottomAlignSuperscriptSpan() {}
    //sets the shift percentage
    BottomAlignSuperscriptSpan( float shiftPercentage ) {
        if( shiftPercentage > 0.0 && shiftPercentage < 1.0 )
            this.shiftPercentage = shiftPercentage;
    }
    @Override
    public void updateDrawState( TextPaint tp ) {
        //original ascent
        float ascent = tp.ascent();
        //scale down the font
        tp.setTextSize( tp.getTextSize() / fontScale );
        //get the new font ascent
        float newAscent = tp.getFontMetrics().ascent;
        //move baseline to top of old font, then move down size of new font
        //adjust for errors with shift percentage
//        tp.baselineShift += ( ascent - ascent * shiftPercentage )
//                - (newAscent - newAscent * shiftPercentage );
    }
    @Override
    public void updateMeasureState( TextPaint tp ) {
        updateDrawState( tp );
    }
}

enter image description here

1
Thiago On

The cleanest way in Kotlin is by using Span

val myTitleText = "1024.12hPa"

val spannable = SpannableString(myTitleText)
spannable.setSpan(
    TextAppearanceSpan(context, R.style.myFontSize),
    8, // beginning of hPa
    myTitleText.length, // end of string
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tvMytitle.text = spannable

See my original post: https://stackoverflow.com/a/69404934/3792198

0
Reza Zavareh On

you can bold and resize a part of your string in kotlin

val s = SpannableStringBuilder()
.append("First Part Not Bold And No Resize ")
.bold { scale(1.5f, { append("Second Part By Bold And Resize " }) } 
.append("Third Part Not Bold And No Resize")

yourTextview.text = s
0
Lilya On

I followed @GregotRant's answer and it worked. However, it turned out that SpannableFactory is not supported for a majority of Android devices, so I came up with another solution, inspired by the accepted answer:

private fun setUnitTextStyle(button: CustomButton) {
        val spanText = SpannableString(button.bottomText)
        val value = spanText.split(" ")[0].length
        val unit = spanText.split(" ")[1].length

        spanText.setSpan(TextAppearanceSpan(button.context, R.style. Regular12Dark),
            value, value+unit+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        button.tvBottom.setText(spanText, TextView.BufferType.SPANNABLE)
}