FontUtil - several styling options for text

To add a custom typeface to your app, the preferred way is to use Downloadable Fonts. To support legacy devices, use Pixplicity Letterpress.

This class adds some additional functionality, such as easily setting a typeface on popup- or overflow menus, as well as adding icons.

Usage

In the example code we assume we can load the default typeface for you app from the App class. When using Letterpress or downloadable fonts you probably won’t need this, otherwise it can be implemented this way:

<!-- Add this to the resources and store the font file in the assets: -->
<string name="default_font" translatable="false">fonts/my_custom_font.ttf</string>
public class App extends Application {
    ...
    private static Typeface sTypeface;

    public static Typeface getDefaultTypeface() {
        if (sTypeface == null) {
            sTypeface = Typeface.createFromAsset(getAssets(), getString(R.string.default_font));
        }
        return sTypeface;
    }

FontUtil classes

Include the following 3 classes and adjust or strip where necessary.

package com.pixplicity.example.util;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AlignmentSpan;
import android.text.style.AlignmentSpan.Standard;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

import com.pixplicity.example.App;
import com.pixplicity.example.ui.CenteredImageSpan;
import com.pixplicity.example.ui.CustomTypefaceSpan;

import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;

/**
 * Utility to create a spannable string set with a typeface
 */
public final class FontUtil {

    private FontUtil() {
    }

    /**
     * Applies the default Typeface to the textView
     *
     * @param view The TextView to style
     */
    public static void setTypeface(@NonNull TextView view) {
        view.setTypeface(App.getDefaultTypeface());
    }

    /**
     * Applies the default typeface to a String and returns a
     * Spannable that can be used in any view.
     *
     * @param text The text to style
     * @return The text with styling applied
     */
    @NonNull
    public static SpannableString setTypeface(@NonNull String text) {
        SpannableString spannableString = new SpannableString(text);
        CustomTypefaceSpan span = new CustomTypefaceSpan(App.getDefaultTypeface());
        spannableString.setSpan(span, 0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return spannableString;
    }

    /**
     * Replaces part of a string with an icon. Also sets the default typeface on the text.
     *
     * @param context Used to access the resources
     * @param text    The complete string
     * @param needle  The string to replace with an icon
     * @param icon    The icon
     * @return SpannableStringBuild containing the icon and the text
     */
    @NonNull
    public static SpannableStringBuilder setIcon(@NonNull Context context, @NonNull String text, @NonNull String
            needle, @DrawableRes int icon, int iconSize) {
        SpannableStringBuilder sb = new SpannableStringBuilder(text);
        int pos = text.indexOf(needle);
        int length = needle.length();
        if (pos == -1) {
            throw new IllegalArgumentException("needle should occur in the text; '"
                    + needle + "' not found");
        }
        // ImageSpan to add icon
        Drawable drawable = ContextCompat.getDrawable(context, icon);
        assert drawable != null;
        if (iconSize > 0) {
            drawable.setBounds(0, 0, iconSize, iconSize);
        } else {
            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        }
        ImageSpan span = new CenteredImageSpan(drawable);
        sb.setSpan(span, pos, pos + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        // To horizontally center align:
        //        sb.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, text.length() -
        //        1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        // Spans to set typeface
        if (pos + length > 0) {
            CustomTypefaceSpan face = new CustomTypefaceSpan(App.getDefaultTypeface());
            sb.setSpan(face, 0, pos + length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        if (pos + length < text.length() - 1) {
            CustomTypefaceSpan face = new CustomTypefaceSpan(App.getDefaultTypeface());
            sb.setSpan(face, pos + length, text.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        return sb;
    }

    /**
     * Adds an icon to the start of the given string. Also sets the default typeface on the text.
     *
     * @param context Used to access the resources
     * @param text    The string to add an icon to
     * @param icon    The icon
     * @return SpannableStringBuild containing the icon and the text
     */
    @NonNull
    public static SpannableStringBuilder setIcon(@NonNull Context context, @NonNull String text, @DrawableRes int
            icon) {
        SpannableStringBuilder sb = new SpannableStringBuilder("  " + text);
        // ImageSpan to add icon
        Drawable drawable = ContextCompat.getDrawable(context, icon);
        assert drawable != null;
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        ImageSpan span = new CenteredImageSpan(drawable);
        sb.setSpan(span, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        // Span to set typeface
        CustomTypefaceSpan face = new CustomTypefaceSpan(App.getDefaultTypeface());
        // Vertical align
        sb.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, text.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        sb.setSpan(face, 1, text.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return sb;
    }

    public static SpannableStringBuilder setIcon(Context context, String text, @DrawableRes int
            icon, int color) {
        SpannableStringBuilder sb = new SpannableStringBuilder("  " + text);
        // ImageSpan to add icon
        Drawable drawable = ContextCompat.getDrawable(context, icon);
        assert drawable != null;
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            drawable.setTint(color);
        }
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        ImageSpan span = new CenteredImageSpan(
                drawable,
                DynamicDrawableSpan.ALIGN_BASELINE,
                (int) (drawable.getIntrinsicWidth() * 0.2f));
        sb.setSpan(span, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        // Span to set typeface
        CustomTypefaceSpan face = new CustomTypefaceSpan(App.getDefaultTypeface());
        // Vertical align
        sb.setSpan(new Standard(Alignment.ALIGN_CENTER), 0, text.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        sb.setSpan(face, 1, text.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return sb;
    }

    public static void setTypefaceOnMenu(Menu menu) {
        // Apply styling to overflow menu items
        for (int i = 0; i < menu.size(); i++) {
            setTypefaceOnMenu(menu.getItem(i));
        }
    }

    public static void setTypefaceOnMenu(MenuItem item) {
        // Set typeface
        item.setTitle(setTypeface(item.getTitle().toString()));
        // Example: set icon tint
        // if (item.getIcon() != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        //     Drawable icon = item.getIcon();
        //     icon.setTint(color);
        // }
    }
}

CenteredImageSpan.java

ImageSpan that adds an image to a bit of text but centers it correctly vertically. This can be used to add icons to overflow menus, for example.

package com.pixplicity.example.ui;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;

import java.lang.ref.WeakReference;

/**
 * ImageSpan that adds an image to a bit of text but centers it correctly vertically
 */
public class CenteredImageSpan extends ImageSpan {

    // Extra variables used to redefine the Font Metrics when an ImageSpan is added
    private int initialDescent = 0;
    private int extraSpace = 0;
    private int extraHorSpace = 0;
    private WeakReference<Drawable> mDrawableRef;

    public CenteredImageSpan(final Drawable drawable) {
        this(drawable, DynamicDrawableSpan.ALIGN_BOTTOM);
    }

    public CenteredImageSpan(final Drawable drawable, final int verticalAlignment) {
        this(drawable, verticalAlignment, 0);
    }

    public CenteredImageSpan(final Drawable drawable, final int alignment, final int textPadding) {
        super(drawable, alignment);
        extraHorSpace = textPadding;
    }

    public Rect getBounds() {
        return getDrawable().getBounds();
    }

    @SuppressWarnings("checkstyle:parameterNumber")
    @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 d = getCachedDrawable();

        int drawableHeight = d.getIntrinsicHeight();
        int fontAscent = paint.getFontMetricsInt().ascent;
        int fontDescent = paint.getFontMetricsInt().descent;
        int extraVertSpace = 24;
        int transY = (bottom - d.getBounds().bottom) / 2
                + (drawableHeight - fontDescent + fontAscent) / 2
                // FIXME Hardcoded extra padding!
                + extraVertSpace;

        canvas.save();
        canvas.translate(x, transY);
        d.draw(canvas);
        canvas.restore();
    }

    // Method used to redefined the Font Metrics when an ImageSpan is added
    @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) {
            // Centers the text with the ImageSpan
            if (rect.bottom - (fm.descent - fm.ascent) >= 0) {
                // Stores the initial descent and computes the margin available
                initialDescent = fm.descent;
                extraSpace = rect.bottom - (fm.descent - fm.ascent);
            }
            fm.descent = extraSpace / 2 + initialDescent;
            fm.bottom = fm.descent;
            fm.ascent = -rect.bottom + fm.descent;
            fm.top = fm.ascent;
        }
        return rect.right + extraHorSpace;
    }

    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;
    }
}

CustomTypeFaceSpan.java

Span that supports a custom typeface. Useful for applying a font to a menu or in other places where you want to apply a typeface on a View you have little control over.

package com.pixplicity.example.ui;

import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;

public class CustomTypefaceSpan extends MetricAffectingSpan {

    private final Typeface typeface;

    public CustomTypefaceSpan(final Typeface typeface) {
        this.typeface = typeface;
    }

    @Override
    public void updateDrawState(final TextPaint drawState) {
        apply(drawState);
    }

    @Override
    public void updateMeasureState(final TextPaint paint) {
        apply(paint);
    }

    private void apply(final Paint paint) {
        final Typeface oldTypeface = paint.getTypeface();
        final int oldStyle = oldTypeface != null ? oldTypeface.getStyle() : 0;
        final int fakeStyle = oldStyle & ~typeface.getStyle();

        if ((fakeStyle & Typeface.BOLD) != 0) {
            paint.setFakeBoldText(true);
        }

        if ((fakeStyle & Typeface.ITALIC) != 0) {
            paint.setTextSkewX(-0.25f);
        }

        paint.setTypeface(typeface);
    }
}