Adding multiple clicking regions to an Android TextView

At work I was asked to make a list of tags clickable. The list of tags came from the API in a comma separated list. So it looks like this:

tag1, Tag2, tag3, TAG4, etc. 

This is nice because I don’t need to worry about displaying it correctly. It’s the API’s job to send me the data.

If you take a look at the XML for the Layout, it’s very simple:

 <TextView
android:id="@+id/list_item_tag_cloud"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/list_item_name"
android:layout_marginBottom="4dip"
android:clickable="true"
style="@style/mix_list.tag_cloud" />

And the style is very straightforward too:

<style name="mix_list.tag_cloud">
 <item name="android:maxLines">1</item>
 <item name="android:clickable">true</item>
 <item name="android:ellipsize">end</item>
 <item name="android:singleLine">true</item>
 <item name="android:lineSpacingExtra">1sp</item>
 <item name="android:textAllCaps">true</item>
 <item name="android:fadingEdge">horizontal</item>
 <item name="android:paddingLeft">10dp</item>
 <item name="android:textColor">@color/black</item>
 </style>

The initial requirement was to show tags in a single line and if there were more, just add the ellipsis “…”, and this worked fine. No need to use anything other than a single TextView to display the data. Android took care of everything. All was good until I was asked to make the tags clickable. So if the user taps tag1 the app should react to that and do something (in this case, a new Activity is launched and the tag1 is used to obtain data). After testing different approaches, this is the easiest I could come up with. I didn’t want to mess with the Layout, one TextView was fine and dynamically adding TextViews (with their ClickListeners) would have been a pain in every possible way, so I resorted to the SpannableString and it’s various incarnations. Before the change, I was simply doing this in my ListAdapter (notice I’m using a ViewHolder pattern):

holder.tagcloud.setText(item.tag_list_cache);

I needed to change that now. So I created a small method which does it for me:

makeTagLinks(item.tag_list_cache, holder.tagcloud);

This small method looks like this:

1	private void makeTagLinks(final String text, final TextView tv) {
2		if (text == null || tv == null) {
3			return;
4		}
5		final SpannableString ss = new SpannableString(text);
6		final List items = Arrays.asList(text.split("\\s*,\\s*"));
7		int start = 0, end;
8		for (final String item : items) {
9			end = start + item.length();
10			if (start < end) {
11				ss.setSpan(new MyClickableSpan(item), start, end, 0);
12			}
13			start += item.length() + 2;//comma and space in the original text ;)
14		}
15		tv.setMovementMethod(LinkMovementMethod.getInstance());
16		tv.setText(ss, TextView.BufferType.SPANNABLE);
17	}

Let’s go through the relevant pieces.

The first thing to notice is that we only use one SpannableString and and then we define regions (spans) inside it. List of tags is automatically split thanks to Java (line 6).

Two auxiliary integers are used (start and end) defined in line 7. The values of these will increment as we move through the tags.

So the loop is a Java 5.x iteration though a List<String>. Nothing fancy. (Line 8).

In Line 9 we start the magic. First calculate the end of the Span. In the first iteration, start (and end) are both zero, so the end is basically the size of the first String.

Line 10 is a security check, if the start went beyond the end, just skip it (that means you’re probably done).

Line 11 is the real magic. Defines a new ClickableSpan from start to end. The MyClickableSpan is a small class that we’ll see later, but it could have been any other type of span (Color, style, etc.); you can even define multiple spans for the same region. We don’t use flags, but you should read the Spannable.java class in Android to see what the flags are, but chances are you’re not going to need them.

Line 13 moves the starting point forward. Since the original text had a comma and a space separating the tags, we need to account for these two characters when moving forwards, because we’re iterating the Strings in the List<> but the span has the whole text. So the start adds the item length plus two more spaces (comma and space between tags). The loop iterates and we get a new string. Start is now positioned at the beginning of this string, so we recalculate the end, which is now start plus the new item length.

Rinse and repeat.

The final two lines 15 and 16 are important. If you don’t set the MovementMethod, it’s not going to work.

Like my friend Jeff Atwood uses to say, read the source, Luke. Here’s what the source code for setMovementMethod says:

* Sets the movement method (arrow key handler) to be used for
* this TextView. This can be null to disallow using the arrow keys
* to move the cursor or scroll the view.

Hmmm ok… doesn’t really say much. What is a MovementMethod anyway? It’s a Java Interface (Protocol for those Objective-C guys) that…

* Provides cursor positioning, scrolling and text selection functionality in a {@link TextView}.

So in line 15 we provide the default implementation, so the tags are “moveable” (it sounds weird).

And this is how it looks in the Android source code:


/**
* A movement method that traverses links in the text buffer and scrolls if necessary.
* Supports clicking on links with DPad Center or Enter.
*/

So by passing an instance of LinkMovementMethod, we’re providing out TextView with all this power!

Anyway, in line 16 we just set the text of the TextView to be the SpannableString and we tell it that the Buffer is Spannable. It’s worth looking through the TextView source code to see what all these things do. There’s a lot going on behind the scenes and it’s always interesting to see “how they did it”.

And that’s it. The only mystery is not to reveal what MyClickableSpan is. You will be disappointed.

private class MyClickableSpan extends ClickableSpan {
   private final String mText;
   private MyClickableSpan(final String text) {
      mText = text;
   }
   @Override
   public void onClick(final View widget) {
      mListener.onTagClicked(mText);
   }
}

Yes, that’s all. I just wanted a ClickableSpan that could store a String (the text associated with the “span”).
Wait, what is mListener?
Just an Interface/Callback that was passed to this Adapter that has one method. onTagClicked(String). In my particular case it’s a Fragment that handles this tag clicking thing, but it can be anything you want.

And that’s it! I’m not sure if this is simple or not, the documentation is not very clear and there are way too many classes floating around…

In any case, just experiment and remember your allies: Google and StackOverflow but don’t become a CopyPaster. Read what the code does, try to understand every single line and ask if you don’t understand what it does. You will be surprised how much you can learn by just reading code.