Is it possible to have multiple styles inside a TextView?

Viewed 295013

Is it possible to set multiple styles for different pieces of text inside a TextView?

For instance, I am setting the text as follows:

tv.setText(line1 + "\n" + line2 + "\n" + word1 + "\t" + word2 + "\t" + word3);

Is it possible to have a different style for each text element? E.g., line1 bold, word1 italic, etc.

The developer guide's Common Tasks and How to Do Them in Android includes Selecting, Highlighting, or Styling Portions of Text:

// Get our EditText object.
EditText vw = (EditText)findViewById(R.id.text);

// Set the EditText's text.
vw.setText("Italic, highlighted, bold.");

// If this were just a TextView, we could do:
// vw.setText("Italic, highlighted, bold.", TextView.BufferType.SPANNABLE);
// to force it to use Spannable storage so styles can be attached.
// Or we could specify that in the XML.

// Get the EditText's internal text storage
Spannable str = vw.getText();

// Create our span sections, and assign a format to each.
str.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
str.setSpan(new BackgroundColorSpan(0xFFFFFF00), 8, 19, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
str.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 21, str.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

But that uses explicit position numbers inside the text. Is there a cleaner way to do this?

20 Answers

If you don't feel like using html, you could just create a styles.xml and use it like this:

TextView tv = (TextView) findViewById(R.id.textview);
SpannableString text = new SpannableString(myString);

text.setSpan(new TextAppearanceSpan(getContext(), R.style.myStyle), 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new TextAppearanceSpan(getContext(), R.style.myNextStyle), 6, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

tv.setText(text, TextView.BufferType.SPANNABLE);

Yes, it is possible using SpannedString. If you are using Kotlin, it becomes even easier to do by using core-ktx, as it provides a domain-specific-language (DSL) for doing this:

    val string: SpannedString = buildSpannedString {
        bold {
            append("1111")
        }
        append("Devansh")     
    }

More options provided by it are:

append("Hello There")
bold {
    append("bold")
    italic {
        append("bold and italic")
        underline {
            append("then some text with underline")
        }
    }
}

At last, you can just to:

textView.text = string

Me Too

How about using some beautiful markup with Kotlin and Anko -

import org.jetbrains.anko.*
override fun onCreate(savedInstanceState: Bundle?) {
    title = "Created with Beautiful Markup"
    super.onCreate(savedInstanceState)

    verticalLayout {
        editText {
            hint = buildSpanned {
                append("Italic, ", Italic)
                append("highlighted", backgroundColor(0xFFFFFF00.toInt()))
                append(", Bold", Bold)
            }
        }
    }
}

Created with Beautiful Markup

As Jon said, for me this is the best solution and you dont need to set any text at runtime, only use this custom class HtmlTextView

public class HtmlTextView extends TextView {

  public HtmlTextView(Context context) {
      super(context);
  }

  public HtmlTextView(Context context, AttributeSet attrs) {
      super(context, attrs);
  }

  public HtmlTextView(Context context, AttributeSet attrs, int defStyleAttr) 
  {
      super(context, attrs, defStyleAttr);
  }

  @TargetApi(21)
  public HtmlTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
      super(context, attrs, defStyleAttr, defStyleRes);
  }

  @Override
  public void setText(CharSequence s,BufferType b){
      super.setText(Html.fromHtml(s.toString()),b);
  }

}

and thats it, now only put it in your XML

<com.fitc.views.HtmlTextView
    android:id="@+id/html_TV"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/example_html" />

with your Html String

<string name="example_html">
<![CDATA[
<b>Author:</b> Mr Donuthead<br/>
<b>Contact:</b> me@donut.com<br/>
<i>Donuts for life </i>
]]>

The cleanest way in Kotlin is by using Span

val myTitleText = "Hello World"

val spannable = SpannableString(myTitleText)
spannable.setSpan(
    TextAppearanceSpan(context, R.style.myFontMedium),
    0,
    4,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tvMytitle.text = spannable

From: Hello Word

To: Hello World

Using SpannableString is a good way to achieve that

I use a few functions to make it easy to apply, I will explain the idea of each first and then show the code:

  1. String.getAllIndexOf(pattern: String): This will search the patter on the string and return an index list of where the pattern start. Ex: given the string "abcdefabc" and I call the method passing the "abc" as the searched pattern, the method should return the list: listOf(0, 6)
  2. The is a class to receive the pattern and a list of styles (in case you desire to apply different styles to the same pattern in sequence)
  3. SpannableString.applyStyle(context: Context, vararg patternAndStyles: PatternAndStyles): This will apply the styles to the given patterns

Now, on code:

  1. getAllIndexOf

    fun String.getAllIndexOf(pattern: String): List<Int> {
        val allRecordsOfText = mutableListOf<Int>()
    
        var index = 0
        while(index >= 0) {
            val newStart = if (allRecordsOfText.isEmpty()) {
                0
            } else {
                allRecordsOfText.last() + pattern.length
            }
            index = this.subSequence(newStart, this.length).indexOf(pattern)
    
            if (index >= 0) {
                allRecordsOfText.add(newStart + index)
            }
        }
    
        return allRecordsOfText.toList()
    }
    
  2. Class to receive the pattern and styles

    @Parcelize
    class PatternAndStyles(
        val pattern: String,
        val styles: List<Int>
    ) : Parcelable
    
  3. applyStyle

    fun SpannableString.applyStyle(context: Context, vararg patternAndStyles: PatternAndStyles) {
        for (patternStyle in patternAndStyles.toList()) {
    
            this.toString().getAllIndexOf(patternStyle.pattern).forEachIndexed { index, start ->
                val end = start + patternStyle.pattern.length
                val styleIndex = if (patternStyle.styles.size > index) index else patternStyle.styles.size - 1
    
                this.setSpan(
                    TextAppearanceSpan(context, patternStyle.styles[styleIndex]),
                    start,
                    end,
                    SPAN_EXCLUSIVE_EXCLUSIVE
                )
            }
        }
    }
    
  4. How to use it in the end

    val stringToApplyStyle = "abc def abc def"
    val text = SpannableString(stringToApplyStyle)
    text.applyStyle(
        this.applicationContext,
        PatternAndStyles("abc", listOf(R.style.Style1, R.style.Style2)),
        PatternAndStyles("def", listOf(R.style.Style3))
    )
    
  5. The output:
    styles applied to text

As it was passed to styles to the pattern "abc" both of then were used But on the pattern "def" the second record reused the last style given on the list

Related