Tags

, , , , , , , , , , , , , , , , ,

A general myth in Android is that it has only one font, as opposed to iOS’s 50+ fonts. It’s not easy enough to support custom fonts in Android. Hence developers were forced to fall in love with Roboto. However, few manufacturers like Samsung started shipping phones with their custom fonts. Should an application use its own font on Android, or follow Holo-ish theme and use Roboto always? If so, are applications losing their brand identity? How would internet be if all webpages had to use just one font?

Since that’s a big discussion by itself, let’s skip that and investigate if we can support fonts. There are few blogs explaining how one can add new fonts. The general idea here is to add the font to assets in project directory, and apply it to the individual views. Should we have to apply the Typeface individually to each element, after it is inflated? (Does this sound like loading a HTML page with default font, and changing the CSS to a custom font — which loads later?). A better approach would be to add a custom attribute, say font in XML, and use it to specify the path to the font. But font is a heavy resource and shared among many views (a flyweight pattern!). Hence, its better to cache the fonts and use them. So, a direct path to the font file wouldn’t work.

Coming from HTML background, the CSS has a font-family attribute that can be used to specify a font family for a group of classes or HTML elements. How could we use a similar concept in Android? In fact, Android supports such a property starting with JellyBean. However, this still doesn’t support user’s custom fonts. There are various styles of Roboto that can be used — like sans-serif, sans-serif-light or sans-serif-condensed. Let’s tie them all together to build a FontManager that can support a CSS like font-family, which effectively caches resources.

Digging into the Android SDK, I found out how Android specifies the list of fonts it supports. If you look into the file: _sdk-directory_/platforms/android-16/data/fonts/system_fonts.xml, you could see a similar XML like below. A family specifies a particular font-type. This has a list of names that it can respond to, specified in nameset and a list of files that specifies the different styles, specified in fileset. And, familyset is a parent tag, more like the resources tag.

<familyset>
    <family>
        <nameset>
            <name>sans-serif</name>
            <name>arial</name>
            <name>helvetica</name>
            <name>tahoma</name>
            <name>verdana</name>
        </nameset>
        <fileset>
            <file>Roboto-Regular.ttf</file>
            <file>Roboto-Bold.ttf</file>
            <file>Roboto-Italic.ttf</file>
            <file>Roboto-BoldItalic.ttf</file>
        </fileset>
    </family>
</familyset>

For the sake of easy understanding, lets create our parser based on this. Since any file in res/values/ expects tags only known to Android, we can add a file with these tags in a file, say fonts.xml in res/xml/ folder. I downloaded a couple of fonts from Google web fonts, added it to assets folder, and specified them in a XML like below. Here, Montaga has just one style — Regular, while OpenSans supports four different styles. Also, OpenSans could respond to “opensans”, “open-sans”, “sans” or “sans-serif” as font-family.

<?xml version="1.0" encoding="utf-8"?>
<familyset>
    
    <!--  Montaga -->
    <family>
        <nameset>
            <name>montaga</name>
        </nameset>
        <fileset>
            <file>fonts/Montaga-Regular.ttf</file>
        </fileset>
    </family>
    
    
    <!--  Open Sans -->
    <family>
        <nameset>
            <name>opensans</name>
            <name>open-sans</name>
            <name>sans</name>
            <name>sans-serif</name>
        </nameset>
        <fileset>
            <file>fonts/OpenSans-Regular.ttf</file>
            <file>fonts/OpenSans-Bold.ttf</file>
            <file>fonts/OpenSans-Italic.ttf</file>
            <file>fonts/OpenSans-BoldItalic.ttf</file>
        </fileset>
    </family>

</familyset>

Since we are adding a custom attribute, we need to create custom views. Also, fonts work as a combination of Typeface and the style (bold/italic/bold and italic/normal). We could use android:textStyle in our custom styleable attribute Fonts.

<resources>
    <declare-styleable name="Fonts">
        <!-- using android's -->
        <attr name="android:textStyle" />

        <!-- our custom attribute -->
        <attr name="fontFamily" format="string" />
    </declare-styleable>
</resources>

And the custom TextView will be like,

    public class FontView extends TextView {
        public FontView(Context context, AttributeSet attrs) {
            super(context, attrs);
		
            // Fonts work as a combination of particular family and the style. 
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Fonts);
            String family = a.getString(R.styleable.Fonts_fontFamily);
            int style = a.getInt(R.styleable.Fonts_android_textStyle, -1);
            a.recycle();

            // Set the typeface based on the family and the style combination.
            setTypeface(FontManager.getInstance().get(family, style));
       }
    }

And the parser for the XML,

public class FontManager {
	
    //Making FontManager a singleton class
    private static class InstanceHolder {
        private static final FontManager INSTANCE = new FontManager();
    }

    public static FontManager getInstance() {
       return FontManager.InstanceHolder.INSTANCE;
    }
	
    private FontManager() { }
    
    // Different tags used in XML file.
    private static final String TAG_FAMILY = "family";
    private static final String TAG_NAMESET = "nameset";
    private static final String TAG_NAME = "name";
    private static final String TAG_FILESET = "fileset";
    private static final String TAG_FILE = "file";
	
    // Different styles supported.
    private static final String STYLE_BOLD = "-Bold.ttf";
    private static final String STYLE_ITALIC = "-Italic.ttf";
    private static final String STYLE_BOLDITALIC = "-BoldItalic.ttf";
	
    private class FontStyle {
        int style;
        Typeface font;
    }
	
    private class Font {
        // different font-family names that this Font will respond to.
        List<String> families;
		
        // different styles for this font.
        List<FontStyle> styles;
    }
	
	private List<Font> mFonts;

	//private boolean isFamilySet = false;
	private boolean isName = false;
	private boolean isFile = false;
	
	// Parse the resId and initialize the parser.
	public void initialize(Context context, int resId) {
		XmlResourceParser parser = null;
		try {
			parser = context.getResources().getXml(resId);
			mFonts = new ArrayList<Font>();

			String tag;
			int eventType = parser.getEventType();
			
			Font font = null;

			do {
				tag = parser.getName();

				switch (eventType) {
					case XmlPullParser.START_TAG:
						if (tag.equals(TAG_FAMILY)) {
							// one of the font-families.
							font = new Font();
						} else if (tag.equals(TAG_NAMESET)) {
							// a list of font-family names supported.
							font.families = new ArrayList<String>();
						} else if (tag.equals(TAG_NAME)) {
							isName = true;
						} else if (tag.equals(TAG_FILESET)) {
							// a list of files specifying the different styles.
							font.styles = new ArrayList<FontStyle>();
						} else if (tag.equals(TAG_FILE)) {
							isFile = true;
						}
						break;

					case XmlPullParser.END_TAG:
						if (tag.equals(TAG_FAMILY)) {
							// add it to the list.
							if (font != null) {
								mFonts.add(font);
								font = null;
							}
						} else if (tag.equals(TAG_NAME)) {
							isName = false;
						} else if (tag.equals(TAG_FILE)) {
							isFile = false;
						}
					break;
					
					case XmlPullParser.TEXT:
						String text = parser.getText();
						if (isName) {
							// value is a name, add it to list of family-names.
							if (font.families != null)
								font.families.add(text);
						} else if (isFile) {
							// value is a file, add it to the proper kind.
							FontStyle fontStyle = new FontStyle();
							fontStyle.font = Typeface.createFromAsset(context.getAssets(), text);
							
							if (text.endsWith(STYLE_BOLD))
								fontStyle.style = Typeface.BOLD;
							else if (text.endsWith(STYLE_ITALIC))
								fontStyle.style = Typeface.ITALIC;
							else if (text.endsWith(STYLE_BOLDITALIC))
								fontStyle.style = Typeface.BOLD_ITALIC;
							else
								fontStyle.style = Typeface.NORMAL;
							
							font.styles.add(fontStyle);
						}
					}

				eventType = parser.next();

			} while (eventType != XmlPullParser.END_DOCUMENT);

		} catch (XmlPullParserException e) {
			throw new InflateException("Error inflating font XML", e);
		} catch (IOException e) {
			throw new InflateException("Error inflating font XML", e);
		} finally {
			if (parser != null)
				parser.close();
		}
	}
	
	public Typeface get(String family, int style) {
		for (Font font: mFonts) {
			for (String familyName : font.families) {
				if (familyName.equals(family)) {
					// if no style in specified, return normal style.
					if (style == -1)
						style = Typeface.NORMAL;
					
					for (FontStyle fontStyle : font.styles) {
						if (fontStyle.style == style)
							return fontStyle.font;
					}
				}
			}
		}
			
	    return null;
	}
}

Now the activity should make sure to load the fonts, so that when the views are inflated, they know to get the font from the FontManager. The following piece of code needs to be added before setContentView() in onCreate().

    // Set up the font parser with the fonts.xml resource.
    FontManager.getInstance().initialize(this, R.xml.fonts);

With so much of plumbing what did we achieve? Quite a lot! Lets see what all this can do.

    <!-- In any layout, we could specify the fontFamily to the custom views.
         "sirius" is our custom namespace for our application -->

    <!-- Regular font -->
    <FontView style="@style/CustomFont"
              android:text="Montaga"
              sirius:fontFamily="montaga"/>
    
    <!-- fontFamily as opensans -->
    <FontView style="@style/CustomFont"
              android:text="OpenSans - Regular"
              sirius:fontFamily="opensans"/>

    <!-- textStyle is "italic" and family is "sans" -->
    <FontView style="@style/CustomFont"
              android:text="OpenSans - Italic"
              android:textStyle="italic"
              sirius:fontFamily="sans"/>

    <!-- The font family can be specified in the styles.xml -->
    <FontView style="@style/OpenSans.Bold" />


    <!-- In styles.xml, we can create hierarchies of styles with different fonts.
         Note that the attribute names are unbounded, as they are in our scope. -->
    <style name="OpenSans">
        <item name="fontFamily">opensans</item>
    </style>

    <style name="OpenSans.Bold">
        <item name="android:textStyle">bold</item>
    </style>

    <style name="OpenSans.Bold.Italic">
        <item name="android:textStyle">bold|italic</item>
    </style>

So, any view that can support custom fonts, can either specify it in its own attribute list, or as a part of it’s style. In fact, we could specify this attribute in themes.xml, and let our application use a default font! Did we just make it look like a HTML page? Oh wait! Did we just allow developers to develop applications with… Comic Sans MS?

Note: The fonts used here are for demonstration purposes only. I don’t own any right to ship it (as I don’t understand the big complex font licensing that Google talks about).

About these ads