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