ahmad
8/9/2016 - 5:39 PM

PatternEditableBuilder - Easy way to create colored clickable spans within a TextView!

PatternEditableBuilder - Easy way to create colored clickable spans within a TextView!

PatternEditableBuilder

This Android utility is about making clickable colored spans within a TextView as painless and simple as possible! One common use case for this is building a Twitter or Facebook type app where different words in a message have different meanings and can be clicked to trigger an action. For example:

  • Tweet items where "@foo" can be clicked to view a user's profile.
  • Facebook posts where "Billy Jean" can be clicked to view a user.
  • Slack messges where "#general" can be clicked to go to a different room.

Usage

This utility assumes you have one or more TextView that are filled with text that you'd like to "spannify" based on different patterns. Suppose we have a TextView that contains the following totally unstyled text:

Linkify Pattern

Linkify all sub-strings within TextView that match a regular expression:

new PatternEditableBuilder().
         addPattern(Pattern.compile("\\@(\\w+)")).
         into(textView);

This results in:

Linkify Pattern with Text Color

Linkify all sub-strings within TextView that match a regular expression and then set color:

new PatternEditableBuilder().
        addPattern(Pattern.compile("\\@(\\w+)"), Color.CYAN).
        into(textView);

This results in:

Linkify Pattern with Color and Click

Linkify all sub-strings within TextView that match a pattern and then set color, and click handler:

new PatternEditableBuilder().
    addPattern(Pattern.compile("\\@(\\w+)"), Color.BLUE,
       new PatternEditableBuilder.SpannableClickedListener() {
            @Override
            public void onSpanClicked(String text) {
                Toast.makeText(MainActivity.this, "Clicked username: " + text,
                    Toast.LENGTH_SHORT).show();
            }
       }).into(textView);

This results in:

Linkify with Custom Span Style

Linkify all sub-strings within TextView that match a pattern and then set custom styles:

new PatternEditableBuilder().
    addPattern(Pattern.compile("\\@(\\w+)"), 
    new PatternEditableBuilder.SpannableStyleListener() {
        @Override
        void onSpanStyled(TextPaint ds) {
            // ds contains everything you need to style the span
            ds.bgColor = Color.GRAY;
            ds.linkColor = Color.MAGENTA;
        }
    }).into(textView);

and this results in:

Complete Example

This is a more complete example which matches both usernames and hashtags:

new PatternEditableBuilder().
    addPattern(Pattern.compile("\\@(\\w+)"), Color.GREEN, 
    new PatternEditableBuilder.SpannableClickedListener() {
        @Override
        public void onSpanClicked(String text) {
            Toast.makeText(MainActivity.this, "Clicked username: " + text,
                    Toast.LENGTH_SHORT).show();
        }
    }).
    addPattern(Pattern.compile("\\#(\\w+)"), Color.CYAN, 
    new PatternEditableBuilder.SpannableClickedListener() {
        @Override
        public void onSpanClicked(String text) {
            Toast.makeText(MainActivity.this, "Clicked hashtag: " + text,
                    Toast.LENGTH_SHORT).show();
        }
    }).into(textView);

and this results in:

Attribution

Created by Nathan Esquenazi from CodePath in 2016.

References

import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*
     Create clickable spans within a TextView
     made easy with pattern matching!

     Created by: Nathan Esquenazi

     Usage 1: Apply spannable strings to a TextView based on pattern

        new PatternEditableBuilder().
           addPattern(Pattern.compile("\\@(\\w+)")).
           into(textView);

     Usage 2: Apply clickable spans to a TextView

         new PatternEditableBuilder().
             addPattern(Pattern.compile("\\@(\\w+)"), Color.BLUE,
             new PatternEditableBuilder.SpannableClickedListener() {
                @Override
                public void onSpanClicked(String text) {
                    // Do something here
                }
             }).into(textView);

     See README for more details.
 */

public class PatternEditableBuilder {
    // Records the pattern spans to apply to a TextView
    ArrayList<SpannablePatternItem> patterns;

    /* This stores a particular pattern item
       complete with pattern, span styles, and click listener */
    public class SpannablePatternItem {
        public SpannablePatternItem(Pattern pattern, SpannableStyleListener styles, SpannableClickedListener listener) {
            this.pattern = pattern;
            this.styles = styles;
            this.listener = listener;
        }

        public SpannableStyleListener styles;
        public Pattern pattern;
        public SpannableClickedListener listener;
    }

    /* This stores the style listener for a pattern item
       Used to style a particular category of spans */
    public static abstract class SpannableStyleListener {
        public int spanTextColor;

        public SpannableStyleListener() {
        }

        public SpannableStyleListener(int spanTextColor) {
            this.spanTextColor = spanTextColor;
        }

        abstract void onSpanStyled(TextPaint ds);
    }

    /* This stores the click listener for a pattern item
       Used to handle clicks to a particular category of spans */
    public interface SpannableClickedListener {
        void onSpanClicked(String text);
    }

    /* This is the custom clickable span class used
       to handle user clicks to our pattern spans
       applying the styles and invoking click listener.
     */
    public class StyledClickableSpan extends ClickableSpan {
        SpannablePatternItem item;

        public StyledClickableSpan(SpannablePatternItem item) {
            this.item = item;
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            if (item.styles != null) {
                item.styles.onSpanStyled(ds);
            }
            super.updateDrawState(ds);
        }

        @Override
        public void onClick(View widget) {
            if (item.listener != null) {
                TextView tv = (TextView) widget;
                Spanned span = (Spanned) tv.getText();
                int start = span.getSpanStart(this);
                int end = span.getSpanEnd(this);
                CharSequence text = span.subSequence(start, end);
                item.listener.onSpanClicked(text.toString());
            }
            widget.invalidate();
        }
    }

    /* ----- Constructors ------- */
    public PatternEditableBuilder() {
        this.patterns = new ArrayList<>();
    }

    /* These are the `addPattern` overloaded signatures */
    // Each allows us to add a span pattern with different arguments
    public PatternEditableBuilder addPattern(Pattern pattern, SpannableStyleListener spanStyles, SpannableClickedListener listener) {
        patterns.add(new SpannablePatternItem(pattern, spanStyles, listener));
        return this;
    }

    public PatternEditableBuilder addPattern(Pattern pattern, SpannableStyleListener spanStyles) {
        addPattern(pattern, spanStyles, null);
        return this;
    }

    public PatternEditableBuilder addPattern(Pattern pattern) {
        addPattern(pattern, null, null);
        return this;
    }

    public PatternEditableBuilder addPattern(Pattern pattern, int textColor) {
        addPattern(pattern, textColor, null);
        return this;
    }

    public PatternEditableBuilder addPattern(Pattern pattern, int textColor, SpannableClickedListener listener) {
        SpannableStyleListener styles = new SpannableStyleListener(textColor) {
            @Override
            public void onSpanStyled(TextPaint ds) {
                ds.linkColor = this.spanTextColor;
            }
        };
        addPattern(pattern, styles, listener);
        return this;
    }

    public PatternEditableBuilder addPattern(Pattern pattern, SpannableClickedListener listener) {
        addPattern(pattern, null, listener);
        return this;
    }

    /* BUILDER METHODS */

    // This builds the pattern span and applies to a TextView
    public void into(TextView textView) {
        SpannableStringBuilder result = build(textView.getText());
        textView.setText(result);
        textView.setMovementMethod(LinkMovementMethod.getInstance());
    }

    // This builds the pattern span into a `SpannableStringBuilder`
    // Requires a CharSequence to be passed in to be applied to
    public SpannableStringBuilder build(CharSequence editable) {
        SpannableStringBuilder ssb = new SpannableStringBuilder(editable);
        for (SpannablePatternItem item : patterns) {
            Matcher matcher = item.pattern.matcher(ssb);
            while (matcher.find()) {
                int start = matcher.start();
                int end = matcher.end();
                StyledClickableSpan url = new StyledClickableSpan(item);
                ssb.setSpan(url, start, end, 0);
            }
        }
        return ssb;
    }

}