Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use custom fonts in DrawerLayout and NavigationView

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?

like image 433
ydstsh Avatar asked Oct 31 '15 10:10

ydstsh


3 Answers

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.

Step 1: In Your Activity

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

Step 2: Custom TypefaceSpan

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!

Step 3: Add Parts Of Lisa Wray's Library

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.

Step 4: Optional: Custom Fonts In Layouts Using Data Binding

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.

A Note On Data Binding

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
    }
}
like image 147
Michael De Soto Avatar answered Nov 03 '22 01:11

Michael De Soto


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) {
    }
}
like image 38
Omar Mahmoud Avatar answered Nov 03 '22 02:11

Omar Mahmoud


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

like image 27
SABANTO Avatar answered Nov 03 '22 03:11

SABANTO