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.
Understanding this chapter requires that you have read the core chapters, particularly the ones on basic widgets and the input method framework.
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
.
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.
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.
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:
ForegroundColorSpan
and BackgroundColorSpan
for coloring textStyleSpan
, TextAppearanceSpan
, TypefaceSpan
,
UnderlineSpan
, and StrikethroughSpan
for affecting the true
“style” of textAbsoluteSizeSpan
, RelativeSizeSpan
, SuperscriptSpan
, and
SubscriptSpan
for affecting the size (and, in some cases, vertical
position) of the textAnd 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.
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.
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.
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:
[a href="..."]
<b>
<big>
<blockquote>
<br>
<cite>
<dfn>
[div align="..."]
<em>
[font size="..." color="..." face="..."]
<h1>
<h2>
<h3>
<h4>
<h5>
<h6>
<i>
[img src="..."]
<p>
<small>
<strong>
<sub>
<sup>
<tt>
<u>
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.
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.
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.
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>
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:
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);
}
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);
}
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);
}
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:
Figure 529: The RichTextSearch sample, after searching on “can”
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.
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.