1

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.

Lilya
  • 495
  • 6
  • 20
  • You could just have 2 textviews, one for the number and other one for the unit – Gregor Rant Apr 22 '21 at 09:15
  • Hi! Yes, I know. But this is will result in refactoring a lot of code, because the text view is located in a button which is a template. – Lilya Apr 22 '21 at 09:19
  • why not split the string by extracting the unit of measurement and then apply the spans to two substrings separately and the join and set to textview – NIKHIL MAURYA Apr 22 '21 at 09:22
  • That will be one span for each property . `RelativeSizeSpan` , `CustomTypefaceSpan` , `PaddingSpan` and `ForgroundColorSpan` . – ADM Apr 22 '21 at 09:27
  • you can append these configs to SpannableStringBuilder anytime you get a new text. What is the problem? Do you want to set these configs once? – Hassan Alizadeh Apr 22 '21 at 09:27
  • Hi @NIKHILMAURYA, I have already tried this as mentioned in my question. I also updated and added the code of what exactly I've been trying. – Lilya Apr 22 '21 at 09:33
  • Hi @ADM, I've tried setting each span for each property, but no success, unfortunately. – Lilya Apr 22 '21 at 09:48
  • Hi @HassanAlizadeh, yes, I would like to set the configs once. – Lilya Apr 22 '21 at 09:50
  • The length of your text is changing anytime as you said. It is not possible to set the config just once. So you should append the config each time you get a new text from onTextWatcher. But you can implement it in an optimized way. – Hassan Alizadeh Apr 22 '21 at 12:32
  • As far as I can understand you don't want to calculate spannableString everytime the pressure is changing? in this case taking two text views is the best way if you don't wanna go custom view route. – NIKHIL MAURYA Apr 22 '21 at 18:33

5 Answers5

2

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)
Gregor Rant
  • 346
  • 2
  • 11
2

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
Reza Zavareh
  • 187
  • 3
  • 7
1

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)
}
Lilya
  • 495
  • 6
  • 20
0

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

Fuad Saneen
  • 354
  • 1
  • 10
  • Hi @Fuad, unfortunately, that didn't work. Plus in my code I would like to avoid adding the class for changing something small. I'd rather want to achieve setting different styles locally when I style my button and its text. – Lilya Apr 22 '21 at 10:21
0

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

Thiago
  • 12,778
  • 14
  • 93
  • 110