Skip to content

Instantly share code, notes, and snippets.

@nesquena
Last active October 21, 2022 10:20
Show Gist options
  • Save nesquena/f2504c642c5de47b371278ee61c75124 to your computer and use it in GitHub Desktop.
Save nesquena/f2504c642c5de47b371278ee61c75124 to your computer and use it in GitHub Desktop.
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;
}
}
@mochadwi
Copy link

mochadwi commented Aug 11, 2020

For heavy task markdown-like with support custom span & almost every markdown-features, you can use this libraries: https://noties.io/Markwon/docs/v4/core/spans-factory.html#appendfactory-prependfactory

@dev990
Copy link

dev990 commented Sep 24, 2020

This is not able to use it in the recycleview .

@AkashcpApps
Copy link

Thank you Bro...

@jo0707
Copy link

jo0707 commented Jun 28, 2021

Thanks! this is perfectly what I needed!

@Kauavitorio
Copy link

Thanks a lot bro

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment