How to implment Hashtags and Callouts in Android

03.24.2014

Have you ever wondered how to implement #hashtags and @callouts in your app? If you're working with the Instagram or Twitter API, you probably will have to make the decision of implementing this at some point.

Thankfully, Android makes it really easy to implement this. There's going to be quite a bit of code here, you can view all of it in the git repository for the project.

Here's some video of the project in action.

Main Activity

Let's first look at our Main Activity. In this case, Comment is a class that just holds two strings, a username and comment text.

package com.zuno.linkdemo;
import android.app.ActionBar;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

import java.util.ArrayList;

public class MainActivity extends Activity {
private String[] names = {"AboveSlipk","Alexify","Anitararo","Arezance",
                          "Candbric","Chickerna","ChirpMarcs","Choraxio","Coolucouse",
                          "Czardion","DariIzZero","Dreamergy","Fantasynell","Girleyerbo",
                          "Goldani","Indategr","Jaeckta","LabsHannahHello","LetterMel","LinkFighter",
                          "Narrativera","Packhozoist","Punkeniamo","QuickXp","RidaSummerGodzilla",
                          "Rockfish","RozFirst","Specialgi","Starusinor","StrongerMania"};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);

    ArrayList<Comment> comments = new ArrayList<Comment>();
    LoremIpsum loremIpsum = new LoremIpsum();


    for(int i = 0; i < 30; i++) {
        comments.add(
                new Comment(
                    names[i],
                    loremIpsum.getWords( (int)(Math.random() * 24) + 5)
                )
        );
    }

    CommentsAdapter adapter = new CommentsAdapter(comments, this);
    ListView commentsList = (ListView) this.findViewById(R.id.comment_list);
    commentsList.setAdapter(adapter);

 }
}

It's simple enough, we wrote a custom adapter to hold our comments and we're giving them usernames and giving them some filler text.

CommentsAdapter

So the magic of the implementation starts here. We're going to use ClickableSpan for our links. How it works is it takes the start index inside a string and then the end and makes the substring something clickable. Pretty neat, right?

Since @callouts and #hashtags start with some symbol in the beginning, we can generalize how we search for our clickable strings. We're going to use regular expressions to find our substrings then we go through our matches and store our starts and ends in a list.

The cool thing is with this you can support any prefix. getSpans(stuff, '+') will match Google+ style callouts, getSpans(stuff, '#') will do it for hashtags and so on.

public ArrayList<int[]> getSpans(String body, char prefix) {
    ArrayList<int[]> spans = new ArrayList<int[]>();

    Pattern pattern = Pattern.compile(prefix + "\\w+");
    Matcher matcher = pattern.matcher(body);

    // Check all occurrences
    while (matcher.find()) {
        int[] currentSpan = new int[2];
        currentSpan[0] = matcher.start();
        currentSpan[1] = matcher.end();
        spans.add(currentSpan);
    }

    return  spans;
}

So now that we can know where our clickables are in our string, how do we link to them? We use can use ClickableSpan. For convenience sake, I wrote two classes Hashtag and CalloutLink that subclassed ClickableSpan.

public class Hashtag extends ClickableSpan{
Context context;
TextPaint textPaint;
public Hashtag(Context ctx) {
    super();
    context = ctx;
}

@Override
public void updateDrawState(TextPaint ds) {
    textPaint = ds;
    ds.setColor(ds.linkColor);
    ds.setARGB(255, 30, 144, 255);
}

@Override
public void onClick(View widget) {
    TextView tv = (TextView) widget;
    Spanned s = (Spanned) tv.getText();
    int start = s.getSpanStart(this);
    int end = s.getSpanEnd(this);
    String theWord = s.subSequence(start + 1, end).toString();
    Toast.makeText(context, String.format("Tags for tags: %s", theWord), 10 ).show();

}
}

 

public class CalloutLink extends ClickableSpan {
Context context;
public CalloutLink(Context ctx) {
    super();
    context = ctx;
}

@Override
public void updateDrawState(TextPaint ds) {
    ds.setARGB(255, 51, 51, 51);
    ds.setTypeface(Typeface.DEFAULT_BOLD);

}

@Override
public void onClick(View widget) {
    TextView tv = (TextView) widget;
    Spanned s = (Spanned) tv.getText();
    int start = s.getSpanStart(this);
    int end = s.getSpanEnd(this);
    String theWord = s.subSequence(start + 1, end).toString();
    Toast.makeText(context, String.format("Here's a cool person: %s", theWord), 10).show();

}
}

In both of these classes, it grabs the content of the spanned text and displays a toast with the content when clicked. In a more useful app, you could wrap it up in a Bundle and pass it along to another Activity or Fragment for a profile view.

Now let's apply these to our TextView.

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        LayoutInflater mInflater =  (LayoutInflater) context
                .getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        convertView = mInflater.inflate(R.layout.listitem_comment, null);

        this.holder = new Holder();
        this.holder.comments = (TextView) convertView.findViewById(R.id.comment_text);
        this.holder.username = (TextView) convertView.findViewById(R.id.username);

        convertView.setTag(this.holder);
    } else {
        this.holder = (Holder) convertView.getTag();
    }

    this.holder.username.setText(this.comments.get(position).getUserName().toUpperCase());
    String commentsText = this.comments.get(position).getComment();

    ArrayList<int[]> hashtagSpans = getSpans(commentsText, '#');
    ArrayList<int[]> calloutSpans = getSpans(commentsText, '@');

    SpannableString commentsContent =
            new SpannableString(this.comments.get(position).getComment());

    for(int i = 0; i < hashtagSpans.size(); i++) {
        int[] span = hashtagSpans.get(i);
        int hashTagStart = span[0];
        int hashTagEnd = span[1];

        commentsContent.setSpan(new Hashtag(context),
                hashTagStart,
                hashTagEnd, 0);

    }

    for(int i = 0; i < calloutSpans.size(); i++) {
        int[] span = calloutSpans.get(i);
        int calloutStart = span[0];
        int calloutEnd = span[1];

        commentsContent.setSpan(new CalloutLink(context),
                calloutStart,
                calloutEnd, 0);

    }


    this.holder.comments.setMovementMethod(LinkMovementMethod.getInstance());
    this.holder.comments.setText(commentsContent);
    return convertView;
}
}

We use the getSpans() method above to get our spans for words beginning with # and @.

Finally, in order to apply a span to the text, we have to put the string in a SpannableString. After that, we can call setSpan() and apply our ClickableSpan to our SpannableString.

And that's all there is to it. Originally, I would have thought about using a WebView for implementing this but I did this instead after knowing how simple it was.