Rich Text

Plain text is so, well, plain.

Fortunately, Android has fairly extensive support for formatted text, before you need to break out something as heavy-weight as WebView. However, some of this rich text support has been shrouded in mystery, particularly how you would allow users to edit formatted text.

This chapter will explain how the rich text support in Android works and how you can take advantage of it, with particular emphasis on some open source projects to help you do just that.

Prerequisites

Understanding this chapter requires that you have read the core chapters, particularly the ones on basic widgets and the input method framework.

The Span Concept

You may have noticed that many methods in Android accept or return a CharSequence. The CharSequence interface is little used in traditional Java, if for no other reason than there are relatively few implementations of it outside of String. However, in Android, CharSequence becomes much more important, because of a sub-interface named Spanned.

Spanned defines sequences of characters (CharSequence) that contain inline markup rules. These rules — mostly instances of CharacterStyle and ParagraphStyle subclasses – indicate whether the “spanned” portion of the characters should be rendered in an alternate font, or be turned into a hyperlink, or have other effects applied to them.

Methods that take a CharSequence as a parameter, therefore, can work equally well with String objects as well as objects that implement Spanned.

Implementations

The base interface for rich-text CharSequence objects is Spanned. This is used for any CharSequence that has inline markup rules, and it defines methods for retrieving markup rules applied to portions of the underlying text.

The primary concrete implementation of Spanned is SpannedString. SpannedString, like String, is immutable — you cannot change either the text or the formatting of a SpannedString.

There is also the Spannable sub-interface of Spanned. Spannable is used for any CharSequence with inline markup rules that can be modified, and it defines the methods for modifying the formatting. There is a corresponding SpannableString implementation.

Finally, there is a related Editable interface, which is for a CharSequence that can have its text modified in-place. SpannableStringBuilder implements both Editable and Spannable, for modifying text and formatting at the same time.

TextView and Spanned

One of the most important uses of Spanned objects is with TextView. TextView is capable of rendering a Spanned, complete with all of the specified formatting. So, if you have a Spanned that indicates that the third word should be rendered in italics, TextView will faithfully italicize that word.

TextView, of course, is an ancestor of many other widgets, from EditText to Button to CheckBox. Each of those, therefore, can use and render Spannable objects. The fact that EditText has the ability to render Spanned objects — and even allow them to be edited — is key for allowing users to enter rich text themselves as part of your UI.

Available Spans

As noted above, the markup rules come in the form of instances of base classes known as CharacterStyle and ParagraphStyle. Despite those names, most of the SDK-supplied subclasses of CharacterStyle and ParagraphStyle end in Span (not Style), and so you will likely see references to these as “spans” as often as “styles”. That also helps minimize confusion between character styles and style resources.

There are well over a dozen supplied CharacterStyle subclasses, including:

  1. ForegroundColorSpan and BackgroundColorSpan for coloring text
  2. StyleSpan, TextAppearanceSpan, TypefaceSpan, UnderlineSpan, and StrikethroughSpan for affecting the true “style” of text
  3. AbsoluteSizeSpan, RelativeSizeSpan, SuperscriptSpan, and SubscriptSpan for affecting the size (and, in some cases, vertical position) of the text

And so on. Similarly, ParagraphStyle has subclasses like BulletSpan for bulleted lists.

You can implement your own custom subclasses of CharacterStyle and ParagraphStyle, though the book does not cover this subject at this time.

Loading Rich Text

Spanned objects do not appear by magic. Plenty of things in Java will give you ordinary strings, from XML and JSON parsers to loading data out of a database to simply hard-coding string constants. However, there are only a few ways that you as a developer will get a Spanned complete with formatting, and that includes you creating such a Spanned yourself by hand.

String Resource

The primary way most developers get a Spanned object into their application is via a string resource. String resources support inline markup in the form of HTML tags. Bold (<b>), italics (<i>), and underline (<u>) are officially supported, such as:


<string name="welcome">Welcome to <b>Android</b>!</string>

When you retrieve the string resource via getText(), you get back a CharSequence that represents a Spanned object with the markup rules in place.

HTML

The next-most common way to get a Spanned object is to use Html.fromHtml(). This parses an HTML string and returns a Spanned object, with all recognized tags converted into corresponding spans. You might use this for text loaded from a database, retrieved from a Web service call, extracted from an RSS feed, etc.

Unfortunately, the list of tags that fromHtml() understands is undocumented. Based upon the source code to fromHtml(), the following seem safe:

However, do bear in mind that these are undocumented and therefore are subject to change. Also note that fromHtml() is perhaps slower than you might think, particularly for longer strings.

You might also wind up using some other support code to get your HTML. For example, some data sources might publish text formatted as Markdown — Stack Overflow, GitHub, etc. use this extensively. Markdown can be converted to HTML, through any number of available Java libraries or via CWAC-AndDown, which wraps the native hoedown Markdown-to-HTML converter for maximum speed. CWAC-AndDown will be explored in a bit more detail in the chapter on the NDK.

From EditText

The reason why so much sample code calls getText() followed by toString() on an EditText widget is because EditText is going to return an Editable object from getText(), not a simple string. That’s because, in theory, EditText could be returning something with formatting applied. The call to toString() simply strips out any potential formatting as part of giving you back a String.

However, you could elect to use the Editable object (presumably a SpannableStringBuilder) if you wanted, such as for pouring the entered text into a TextView, complete with any formatting that might have wound up on the entered text.

Manually

You are welcome to create a SpannableString via its constructor, supplying the text that you wish to display, then calling various methods on SpannableString to format it.

Or, you are welcome to create a SpannableStringBuilder via its constructor. In some respects, SpannableStringBuilder works like the classic StringBuilder — you call append() to add more text. However, SpannableStringBuilder also offers delete(), insert(), and replace() methods to modify portions of the existing content. It also supports the same methods that SpannableString does, via the Spannable interface, for applying formatting rules to portions of text.

Editing Rich Text

If the Spannable you wound up with is a SpannedString, it is what it is — you cannot change it. If, however, you have a SpannableString, that can be modified by you, or by the user. Of course, allowing the user to modify a Spannable gets a wee bit tricky, and is why the RichEditText project was born.

Spannable offers two methods for modifying its formatting: setSpan() to apply formatting, and removeSpan() to get rid of an existing span. And, since Spannable extends Spanned, a Spannable also has getSpans(), to return existing spans of a current type within a certain range of characters in the text. These methods, along with others on Spanned, allow you to get and set whatever formatting you wish to apply on a Spannable object, such as a SpannableString.

For example, let’s take a look at the RichText/Search sample project. Here, we are going to load some text into a TextView, then allow the user to enter a search string in an EditText, and we will use the Spannable methods to highlight the search string occurrences inside the text in the TextView.

Our layout is simply an EditText atop a TextView (wrapped in a ScrollView):

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

  <EditText
    android:id="@+id/search"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:singleLine="true">

    <requestFocus/>
  </EditText>

  <ScrollView
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
      android:id="@+id/prose"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/address"
      android:textAppearance="?android:attr/textAppearanceMedium"/>
  </ScrollView>

</LinearLayout>
(from RichText/Search/app/src/main/res/layout/main.xml)

We pre-fill the TextView with a string resource (@string/address), which in this project is the text of Lincoln’s Gettysburg Address, with a bit of inline markup (e.g., “Four score and seven years ago” italicized). So, when we fire up the project at the outset, we see the formatted prose from the string resource:

The RichTextSearch sample, as initially launched
Figure 528: The RichTextSearch sample, as initially launched

In onCreate() of our activity, we find the EditText widget and designate the activity itself as being an OnEditorActionListener for the EditText:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    search=(EditText)findViewById(R.id.search);
    search.setOnEditorActionListener(this);
  }
(from RichText/Search/app/src/main/java/com/commonsware/android/rich/search/RichTextSearchActivity.java)

That means when the user presses <Enter>, we will get control in an onEditorAction() method. There, we pass the search text to a private searchFor() method, plus ensure that the input method editor is hidden (if one was used to fill in the search text):

  @Override
  public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    if (event == null || event.getAction() == KeyEvent.ACTION_UP) {
      searchFor(search.getText().toString());

      InputMethodManager imm=
          (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);

      imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
    }

    return(true);
  }
(from RichText/Search/app/src/main/java/com/commonsware/android/rich/search/RichTextSearchActivity.java)

The searchFor() method is where the formatting is applied to our search text:

  private void searchFor(String text) {
    TextView prose=(TextView)findViewById(R.id.prose);
    Spannable raw=new SpannableString(prose.getText());
    BackgroundColorSpan[] spans=raw.getSpans(0,
                                             raw.length(),
                                             BackgroundColorSpan.class);

    for (BackgroundColorSpan span : spans) {
      raw.removeSpan(span);
    }

    int index=TextUtils.indexOf(raw, text);

    while (index >= 0) {
      raw.setSpan(new BackgroundColorSpan(0xFF8B008B), index, index
          + text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      index=TextUtils.indexOf(raw, text, index + text.length());
    }

    prose.setText(raw);
  }
(from RichText/Search/app/src/main/java/com/commonsware/android/rich/search/RichTextSearchActivity.java)

First, we get a Spannable object out of the TextView. While an EditText returns an Editable from getText(), getText() on a TextView returns a CharSequence. In particular, the first time we execute searchFor(), getText() will return a SpannedString, as that is what a string resource turns into. However, that is not modifiable, so we convert it into a SpannableString so we can apply formatting to it. An optimization would be to see if getText() returns something implementing Spannable and then just using it directly.

We want to highlight the search terms using a BackgroundColorSpan. However, that means we first need to get rid of any existing BackgroundColorSpan objects applied to the prose from a previous search — otherwise, we would keep highlighting more and more of the prose. So, we use getSpans() to find all BackgroundColorSpan objects anywhere in the prose (from index 0 through the length of the text). For each that we find, we call removeSpan() to get rid of it from our Spannable.

Then, we use indexOf() on TextUtils to find the first occurrence of whatever the user typed into the EditText. If we find it, we create a new BackgroundColorSpan and apply it to the matching portion of the prose using setSpan(). The last parameter to setSpan() is a flag, indicating what should happen if text is inserted at either the starting or ending point. In our case, the text itself is remaining constant, so the flag does not matter much – here, we use SPAN_EXCLUSIVE_EXCLUSIVE, which would mean that the span would not cover any text inserted at the starting or ending point of the span.

We then continue using indexOf() to find any remaining occurrences of the search text. Once we are done modifying our Spannable, we put it into the TextView via setText().

The result is that all matching substrings are highlighted in a purple/magenta shade:

The RichTextSearch sample, after searching on can
Figure 529: The RichTextSearch sample, after searching on “can”

Saving Rich Text

SpannableString and SpannedString are not Serializable. There is no built-in way to persist them directly.

However, Html.toHtml() will convert a Spanned object into corresponding HTML, for all CharacterStyle and ParagraphStyle objects that can be readily converted into HTML. You can then persist the resulting HTML any place you would persist a String (e.g., database column).

In principle, you could create other similar conversion code, such as something to take a Spanned and return the corresponding Markdown source.

Manipulating Rich Text

The TextUtils class has many utility methods that manipulate a CharSequence, to allow you to do things that you might ordinarily have done just with methods on String. These utility methods will work with any CharSequence, including SpannedString and SpannableString.

Some are specifically aimed at Spanned objects, such as copySpansFrom() (to apply formatting from one CharSequence onto another). Some are clones of String equivalents, such as split(), join(), and substring(). Yet others are designed for developers using the Canvas 2D drawing API, such as ellipsize() and commaEllipsize() for intelligently truncating messages.