Skip to content

Instantly share code, notes, and snippets.

@tunjid
Created July 8, 2016 12:48
Show Gist options
  • Save tunjid/8318c221a13eaccba459e27dae796ae1 to your computer and use it in GitHub Desktop.
Save tunjid/8318c221a13eaccba459e27dae796ae1 to your computer and use it in GitHub Desktop.
package com.tunjid.demo.Util;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import com.tunjid.demo.Application;
import com.tunjid.demo.R;
import java.lang.ref.WeakReference;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by tj.dahunsi on 13.05.16.
* Provides {@link String#format} style functions that work with {@link Spanned} strings and preserve formatting.
*
* @see <a href="https://github.com/george-steel/android-utils/blob/master/src/org/oshkimaadziig/george/androidutils/SpanFormatter.java">Original source</a>
* @see <a href="http://developer.android.com/guide/topics/resources/string-resource.html">Modifications from Android Developers website</a>
*/
public class SpanUtils {
public static final Pattern FORMAT_SEQUENCE = Pattern.compile("%([0-9]+\\$|<?)([^a-zA-z%]*)([[a-zA-Z%]&&[^tT]]|[tT][a-zA-Z])");
private SpanUtils() {
}
/**
* Returns a CharSequence that concatenates the specified array of CharSequence
* objects and then applies a list of zero or more tags to the entire range.
*
* @param content an array of character sequences to apply a style to
* @param tags the styled span objects to apply to the content
* such as android.text.style.StyleSpan
*/
private static CharSequence apply(CharSequence[] content, Object... tags) {
SpannableStringBuilder text = new SpannableStringBuilder();
openTags(text, tags);
for (CharSequence item : content) {
text.append(item);
}
closeTags(text, tags);
return text;
}
/**
* Iterates over an array of tags and applies them to the beginning of the specified
* Spannable object so that future text appended to the text will have the styling
* applied to it. Do not call this method directly.
*/
private static void openTags(Spannable text, Object[] tags) {
for (Object tag : tags) {
text.setSpan(tag, 0, 0, Spannable.SPAN_MARK_MARK);
}
}
/**
* "Closes" the specified tags on a Spannable by updating the spans to be
* endpoint-exclusive so that future text appended to the end will not take
* on the same styling. Do not call this method directly.
*/
private static void closeTags(Spannable text, Object[] tags) {
int len = text.length();
for (Object tag : tags) {
if (len > 0) {
text.setSpan(tag, 0, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
else {
text.removeSpan(tag);
}
}
}
/**
* Returns a CharSequence that applies boldface to the concatenation
* of the specified CharSequence objects.
*/
public static CharSequence bold(CharSequence... content) {
return apply(content, new StyleSpan(Typeface.BOLD));
}
/**
* Returns a CharSequence that applies italics to the concatenation
* of the specified CharSequence objects.
*/
public static CharSequence italic(CharSequence... content) {
return apply(content, new StyleSpan(Typeface.ITALIC));
}
/**
* Returns a CharSequence that applies a foreground color to the
* concatenation of the specified CharSequence objects.
*/
public static CharSequence color(@ColorRes int color, CharSequence... content) {
Context context = Application.getContext();
int resolvedColor = ContextCompat.getColor(context, color);
return apply(content, new ForegroundColorSpan(resolvedColor));
}
public static CharSequence drawable(@DrawableRes int drawable, CharSequence... content) {
Context context = Application.getContext();
return apply(content, new ImageSpan(context, drawable));
}
public static CharSequence resize(float relativeSize, CharSequence... content) {
return apply(content, new RelativeSizeSpan(relativeSize));
}
public static SpanBuilder spanBuilder(CharSequence content) {
return new SpanBuilder(content);
}
/**
* Version of {@link String#format(String, Object...)} that works on {@link Spanned} strings to preserve rich text formatting.
* Both the {@code format} as well as any {@code %s args} can be Spanned and will have their formatting preserved.
* Due to the way {@link Spannable}s work, any argument's spans will can only be included <b>once</b> in the result.
* Any duplicates will appear as text only.
*
* @param format the format string (see {@link java.util.Formatter#format})
* @param args the list of arguments passed to the formatter. If there are
* more arguments than required by {@code format},
* additional arguments are ignored.
* @return the formatted string (with spans).
*/
public static SpannedString format(CharSequence format, Object... args) {
return format(Locale.getDefault(), format, args);
}
/**
* Version of {@link String#format(Locale, String, Object...)} that works on {@link Spanned} strings to preserve rich text formatting.
* Both the {@code format} as well as any {@code %s args} can be Spanned and will have their formatting preserved.
* Due to the way {@link Spannable}s work, any argument's spans will can only be included <b>once</b> in the result.
* Any duplicates will appear as text only.
*
* @param locale the locale to apply; {@code null} value means no localization.
* @param format the format string (see {@link java.util.Formatter#format})
* @param args the list of arguments passed to the formatter.
* @return the formatted string (with spans).
* @see String#format(Locale, String, Object...)
*/
public static SpannedString format(Locale locale, CharSequence format, Object... args) {
SpannableStringBuilder out = new SpannableStringBuilder(format);
int i = 0;
int argAt = -1;
while (i < out.length()) {
Matcher m = FORMAT_SEQUENCE.matcher(out);
if (!m.find(i)) break;
i = m.start();
int exprEnd = m.end();
String argTerm = m.group(1);
String modTerm = m.group(2);
String typeTerm = m.group(3);
CharSequence cookedArg;
switch (typeTerm) {
case "%":
cookedArg = "%";
break;
case "n":
cookedArg = "\n";
break;
default:
int argIdx;
switch (argTerm) {
case "":
argIdx = ++argAt;
break;
case "<":
argIdx = argAt;
break;
default:
argIdx = Integer.parseInt(argTerm.substring(0, argTerm.length() - 1)) - 1;
break;
}
Object argItem = args[argIdx];
if (typeTerm.equals("s") && argItem instanceof Spanned)
cookedArg = (Spanned) argItem;
else
cookedArg = String.format(locale, "%" + modTerm + typeTerm, argItem);
break;
}
out.replace(i, exprEnd, cookedArg);
i += cookedArg.length();
}
return new SpannedString(out);
}
public static class SpanBuilder {
CharSequence content = "";
private SpanBuilder(CharSequence content) {
this.content = content;
}
public SpanBuilder bold() {
this.content = SpanUtils.bold(content);
return this;
}
public SpanBuilder italic() {
this.content = SpanUtils.italic(content);
return this;
}
public SpanBuilder resize(float relativeSize) {
this.content = SpanUtils.resize(relativeSize, content);
return this;
}
public SpanBuilder color(@ColorRes int color) {
this.content = SpanUtils.color(color, content);
return this;
}
public SpanBuilder prependCenteredDrawable(@DrawableRes int drawableResource) {
Context context = Application.getContext();
CharSequence dummyString = " ";
prependCharsequence(dummyString);
SpannableString result = new SpannableString(content);
ImageSpan centeredImageSpan = new CenteredImageSpan(context, drawableResource);
result.setSpan(centeredImageSpan, 1, 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
content = result;
return this;
}
public SpanBuilder wrapInBrackets() {
Context context = Application.getContext();
content = SpanUtils.format(context.getString(R.string.apply_brackets), content);
return this;
}
public SpanBuilder prependNumber(int number) {
Context context = Application.getContext();
content = SpanUtils.format(context.getString(R.string.prepend_number), number, content);
return this;
}
public SpanBuilder prependCharsequence(CharSequence sequence) {
Context context = Application.getContext();
content = SpanUtils.format(context.getString(R.string.prepend_string), sequence, content);
return this;
}
public SpanBuilder newLine() {
Context context = Application.getContext();
content = SpanUtils.format(context.getString(R.string.prepend_string), "\n", content);
return this;
}
public CharSequence build() {
return content;
}
}
/**
* Created by tj.dahunsi on 14.05.16.
* Image span centered in it's View
*
* @see <a href="http://stackoverflow.com/questions/25628258/align-text-around-imagespan-center-vertical">Stackoverflow</a>
*/
public static class CenteredImageSpan extends ImageSpan {
private WeakReference<Drawable> mDrawableRef;
public CenteredImageSpan(Context context, final int drawableRes) {
super(context, drawableRes);
}
@Override
public int getSize(Paint paint, CharSequence text,
int start, int end,
Paint.FontMetricsInt fm) {
Drawable d = getCachedDrawable();
Rect rect = d.getBounds();
if (fm != null) {
Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
// keep it the same as paint's fm
fm.ascent = pfm.ascent;
fm.descent = pfm.descent;
fm.top = pfm.top;
fm.bottom = pfm.bottom;
}
return rect.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text,
int start, int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
Drawable b = getCachedDrawable();
canvas.save();
int drawableHeight = b.getIntrinsicHeight();
int fontAscent = paint.getFontMetricsInt().ascent;
int fontDescent = paint.getFontMetricsInt().descent;
int transY = bottom - b.getBounds().bottom + // align bottom to bottom
(drawableHeight - fontDescent + fontAscent) / 2; // align center to center
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
// Redefined locally because it is a private member from DynamicDrawableSpan
private Drawable getCachedDrawable() {
WeakReference<Drawable> wr = mDrawableRef;
Drawable d = null;
if (wr != null)
d = wr.get();
if (d == null) {
d = getDrawable();
mDrawableRef = new WeakReference<>(d);
}
return d;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment