How to use custom fonts in DrawerLayout and NavigationView
Asked Answered
W

4

6

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?

Weigle answered 31/10, 2015 at 10:3 Comment(2)
Take a look to this post : #30668846Cenozoic
@Bubu, tnx my dear friendWeigle
G
3

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) {
    }
}
Garrulity answered 31/10, 2015 at 12:41 Comment(3)
which view has to passed while method call ?Harr
you should pass the parent viewGarrulity
uncomplete information, we can't spend our whole day while using this code.Scutari
H
4

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
    }
}
Huth answered 15/11, 2015 at 17:57 Comment(4)
This method almost gave a slow death to my app.Avoid it guys. #No offencePounce
Care to explain why? The Lisa Wray method was specifically called out by Google's Android people at a conference I went to in December.Huth
I implemented it following all the steps you mentioned here..yes it worked..but it took my smoothness away...navigation drawer started flickering. So i removed it and loaded font for every navigation view item..Its working great then.. :)Pounce
I think part of the problem is that the font cache isn't working correctly. When getting, the cache is queried by font name. When putting, the cache is stashed by filename. There's an open PR for this here: github.com/lisawray/fontbinding/pull/4Huth
C
4

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

Certiorari answered 25/6, 2019 at 19:41 Comment(2)
This works, but it makes ALL the fonts in the navigationView the same. What do you do if you want different fonts in the title and the options menu?Huoh
Try adding the style directly to the DrawerLayout in xmlCertiorari
G
3

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) {
    }
}
Garrulity answered 31/10, 2015 at 12:41 Comment(3)
which view has to passed while method call ?Harr
you should pass the parent viewGarrulity
uncomplete information, we can't spend our whole day while using this code.Scutari
D
0

Try apply following attribute to NavigationView in XML instead of a new style.

    app:itemTextAppearance="@style/MyCustomNavStyle"

And In style.xml, add new style,

 <style name="MyCustomNavStyle"> <item 
 name="android:fontFamily">@font/montserrat_regular</item> </style>
Dobbin answered 11/1, 2023 at 5:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.