I want to use Android's DrawerLayout
and NavigationView
for menus, but I don't know how to have the menu items use a custom font. Does anyone have a successful implementation?
Omar Mahmoud's answer will work. But it doesn't make use of font caching, which means you're constantly reading from disk, which is slow. And apparently older devices can leak memory--though I haven't confirmed this. At the very least, it's very inefficient.
Follow Steps 1-3 if all you want is font caching. This is a must do. But let's go above and beyond: Let's implement a solution that uses Android's Data Binding library (credit to Lisa Wray) so that you can add custom fonts in your layouts with exactly one line. Oh, did I mention that you don't have to extend TextView
* or any other Android class?. It's a little extra work, but it makes your life very easy in the long run.
This is what your Activity
should look like:
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FontCache.getInstance().addFont("custom-name", "Font-Filename");
NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_view);
Menu menu = navigationView.getMenu();
for (int i = 0; i < menu.size(); i++)
{
MenuItem menuItem = menu.getItem(i);
if (menuItem != null)
{
SpannableString spannableString = new SpannableString(menuItem.getTitle());
spannableString.setSpan(new TypefaceSpan(FontCache.getInstance(), "custom-name"), 0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
menuItem.setTitle(spannableString);
// Here'd you loop over any SubMenu items using the same technique.
}
}
}
There's not much to this. It basically lifts all the pertinent parts of Android's TypefaceSpan
, but does not extend it. It probably should be named something else:
/**
* Changes the typeface family of the text to which the span is attached.
*/
public class TypefaceSpan extends MetricAffectingSpan
{
private final FontCache fontCache;
private final String fontFamily;
/**
* @param fontCache An instance of FontCache.
* @param fontFamily The font family for this typeface. Examples include "monospace", "serif", and "sans-serif".
*/
public TypefaceSpan(FontCache fontCache, String fontFamily)
{
this.fontCache = fontCache;
this.fontFamily = fontFamily;
}
@Override
public void updateDrawState(TextPaint textPaint)
{
apply(textPaint, fontCache, fontFamily);
}
@Override
public void updateMeasureState(TextPaint textPaint)
{
apply(textPaint, fontCache, fontFamily);
}
private static void apply(Paint paint, FontCache fontCache, String fontFamily)
{
int oldStyle;
Typeface old = paint.getTypeface();
if (old == null) {
oldStyle = 0;
} else {
oldStyle = old.getStyle();
}
Typeface typeface = fontCache.get(fontFamily);
int fake = oldStyle & ~typeface.getStyle();
if ((fake & Typeface.BOLD) != 0) {
paint.setFakeBoldText(true);
}
if ((fake & Typeface.ITALIC) != 0) {
paint.setTextSkewX(-0.25f);
}
paint.setTypeface(typeface);
}
}
Now, we don't have to pass in the instance of FontCache
here, but we do in case you want to unit test this. We all write unit tests here, right? I don't. So if anyone wants to correct me and provide a more testable implementation, please do!
I'd be nice if this library was packaged up so that we could just include it in build.gradle
. But, there's not much to it, so it's not a big deal. You can find it on GitHub here. I'm going to include the required parts for this implementation in case she ever takes the project down. There's another class you'll need to add to use Data Binding in your layouts, but I'll cover that in Step 4:
Your Activity
class:
public class Application extends android.app.Application
{
private static Context context;
public void onCreate()
{
super.onCreate();
Application.context = getApplicationContext();
}
public static Context getContext()
{
return Application.context;
}
}
The FontCache
class:
/**
* A simple font cache that makes a font once when it's first asked for and keeps it for the
* life of the application.
*
* To use it, put your fonts in /assets/fonts. You can access them in XML by their filename, minus
* the extension (e.g. "Roboto-BoldItalic" or "roboto-bolditalic" for Roboto-BoldItalic.ttf).
*
* To set custom names for fonts other than their filenames, call addFont().
*
* Source: https://github.com/lisawray/fontbinding
*
*/
public class FontCache {
private static String TAG = "FontCache";
private static final String FONT_DIR = "fonts";
private static Map<String, Typeface> cache = new HashMap<>();
private static Map<String, String> fontMapping = new HashMap<>();
private static FontCache instance;
public static FontCache getInstance() {
if (instance == null) {
instance = new FontCache();
}
return instance;
}
public void addFont(String name, String fontFilename) {
fontMapping.put(name, fontFilename);
}
private FontCache() {
AssetManager am = Application.getContext().getResources().getAssets();
String fileList[];
try {
fileList = am.list(FONT_DIR);
} catch (IOException e) {
Log.e(TAG, "Error loading fonts from assets/fonts.");
return;
}
for (String filename : fileList) {
String alias = filename.substring(0, filename.lastIndexOf('.'));
fontMapping.put(alias, filename);
fontMapping.put(alias.toLowerCase(), filename);
}
}
public Typeface get(String fontName) {
String fontFilename = fontMapping.get(fontName);
if (fontFilename == null) {
Log.e(TAG, "Couldn't find font " + fontName + ". Maybe you need to call addFont() first?");
return null;
}
if (cache.containsKey(fontFilename)) {
return cache.get(fontFilename);
} else {
Typeface typeface = Typeface.createFromAsset(Application.getContext().getAssets(), FONT_DIR + "/" + fontFilename);
cache.put(fontFilename, typeface);
return typeface;
}
}
}
And that's really all there is to it.
Note: I'm anal about my method names. I've renamed getApplicationContext()
to getContext()
here. Keep that in mind if you're copying code from here and from her project.
Everything above just implements a FontCache. There's a lot of words. I'm a verbose type of a guy. This solution doesn't really get cool unless you do this:
We need to change the Activity
so that we add custom fonts to the cache before setContentView
is called. Also, setContentView
is replaced by DataBindingUtil.setContentView
:
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
FontCache.getInstance().addFont("custom-name", "Font-Filename");
DataBindingUtil.setContentView(this, R.layout.activity_main);
[...]
}
Next, add a Bindings
class. This associates the binding with the XML attribute:
/**
* Custom bindings for XML attributes using data binding.
* (http://developer.android.com/tools/data-binding/guide.html)
*/
public class Bindings
{
@BindingAdapter({"bind:font"})
public static void setFont(TextView textView, String fontName)
{
textView.setTypeface(FontCache.getInstance().get(fontName));
}
}
Finally, in your layout, do this:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<data/>
<TextView
[...]
android:text="Words"
app:font="@{`custom-name`}"/>
That's it! Seriously: app:font="@{``custom-name``}"
. That's it.
The Data Binding docs, at the time of this writing, are a little misleading. They suggest adding a couple things to build.gradle
which will just not work with the latest version of Android Studio. Ignore the gradle related installation advice and do this instead:
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0-beta1'
}
}
android {
dataBinding {
enabled = true
}
}
use this method passing the base view in your drawer
public static void overrideFonts(final Context context, final View v) {
Typeface typeface=Typeface.createFromAsset(context.getAssets(), context.getResources().getString(R.string.fontName));
try {
if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
View child = vg.getChildAt(i);
overrideFonts(context, child);
}
} else if (v instanceof TextView) {
((TextView) v).setTypeface(typeface);
}
} catch (Exception e) {
}
}
Step 1: Create style:
<style name="ThemeOverlay.AppCompat.navTheme">
<item name="colorPrimary">@android:color/transparent</item>
<item name="colorControlHighlight">?attr/colorAccent</item>
<item name="fontFamily">@font/metropolis</item>
</style>
Step 2: Add theme to NavigationView in xml
app:theme="@style/ThemeOverlay.AppCompat.navTheme"
For anyone who might come looking here for a simple solution
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With