19 June, 2013

Custom fonts in Android (Part 3 of 3) - Memory leaks and FontsHelper

As reported here in issue 9904 there is a memory leak if we call Typeface.createFromAsset() multiple times as the fonts are kept open.
To avoid this problem and to keep from accessing assstes multiple times we'll create FontsHelper, a class that will contain all typefaces needed by the app.

public class FontsHelper {

 public static final String ROBOTO_LIGHT = "fonts/Roboto-Light.ttf";
 public static final String ROBOTO_BOLD = "fonts/Roboto-Bold.ttf";
 public static final String ROBOTO_CONDENSED = "fonts/Roboto-Condensed.ttf";
 
 private static Map fonts = new HashMap();
 
 public static Typeface getTypeFace(Context context, String fontPath) {
  
  if (!fonts.containsKey(fontPath)) {
   
   Typeface font = Typeface.createFromAsset(context.getAssets(), fontPath);
   fonts.put(fontPath, font);
  }
  
  return fonts.get(fontPath);
 }
 
 public static void setFont(View view, Typeface font) {
 
        if (view instanceof ViewGroup) 
  {
            for (int i = 0; i < ((ViewGroup)view).getChildCount(); i++) {
   
                setFont(((ViewGroup)view).getChildAt(i), font);
            }
        } else if (view instanceof TextView) {
  
            ((TextView) view).setTypeface(font);
        }
    }
 
 public static void setFont(Context ctx, TextView view, AttributeSet attrs) {

  TypedArray styleAttrs = context.obtainStyledAttributes(attrs, R.styleable.CustomFontView);
  String customFont = styleAttrs.getString(R.styleable.CustomFontView_customFont);
  setFont(context, view, customFont);
  styleAttrs.recycle();
 }

 public static boolean setFont(Context ctx, TextView view, String fontPath) {

  boolean successful = true;
 
  try {
  
   Typeface tf = AssetsHelper.getTypeFace(ctx, fontPath);
   view.setTypeface(tf);
  } catch (Exception e) {
   
   Log.e(TAG, "Error to get typeface: " + e.getMessage());
   successful = false;
  }

  return successful;
 }
This way we avoid the memory leaks and can further simplify the CustomFontTextView class from the previous post.

public class CustomFontTextView extends TextView {

 public CustomFontTextView(Context context, AttributeSet attrs) {

  super(context, attrs);
  FontsHelper.setFont(context, this, attrs);
 }

 public CustomFontTextView(Context context, AttributeSet attrs, int defStyle) {

  super(context, attrs, defStyle);
  FontsHelper.setFont(context, this, attrs);
 }
 
 public CustomFontTextView(Context context, String fontPath) {
  
  super(context);
  FontsHelper.setFont(context, this, fontPath);
 }
}
With the custom font static variables in FontsHelper and the constructor with a fontPath argument in CustomFontTextView we can also apply the font by code in an easier way.

Custom fonts in Android (Part 2 of 3) - Setting font in xml

The objective is to have the following xml:

<?xml version="1.0" encoding="utf-8"?>
<com.demo.LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:control="http://schemas.android.com/apk/res/com.demo"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.demo.CustomFontTextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        control:customFont="fonts/Roboto-Light.ttf" />

</com.demo.LinearLayout>

To enable this we need to declare a custom attribute in values\attrs.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    
    <declare-styleable name="CustomFontView">
        <attr name="customFont" format="string" />
    </declare-styleable>

</resources>
Now we can add the control namespace as seen in the first snippet in line 4.
To read the customFont value and actually apply it to the TextView we're going to extend it and read the new attribute. The CustomFontTextView could be something like this:

public class CustomFontTextView extends TextView {

 public CustomFontTextView(Context context, AttributeSet attrs) {

  super(context, attrs);
  setCustomFont(context, this, attrs);
 }
 
 public void setCustomFont(Context context, TextView view, AttributeSet attrs) {

  TypedArray styleAttrs = context.obtainStyledAttributes(attrs, R.styleable.CustomFontView);
  String customFont = styleAttrs.getString(R.styleable.CustomFontView_customFont);
  setCustomFont(context, view, customFont);
  styleAttrs.recycle();
 }
}
Just like this CustomFontTextView, the same method can be applied to all views. To apply to multiple views inside a complex view check Arnaud's answer in http://stackoverflow.com/questions/9797872/use-roboto-font-for-earlier-deviceshttp://stackoverflow.com/questions/9797872/use-roboto-font-for-earlier-devices.

To avoid having the same path copied all over your xml files you may consider creating styles and put the path there.

<style name="RobotoTextView" parent="android:Widget.Holo.Light.TextView">
 <item name="customFont">fonts/Roboto-Bold.ttf</item>
</style>
And then applying that style in your custom controls:
    <com.demo.CustomFontTextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        style="@style/RobotoTextView" />
On the last part we will focus on a memory leak you may find and how to avoid it.

Custom fonts in Android (Part 1 of 3) - Adding to project and set by code

To use custom fonts in Android add the necessary font files to assets/fonts (you may need to create the assets folder).
To apply them by code use the following snippet:

Typeface font = Typeface.createFromAsset(getAssets(), "fonts/Roboto-Light.ttf");
textView.setTypeface(font);
On the second part of this series we'll apply custom fonts in xml.
UPDATE: Oh, and please don't include unnecessary fonts in the asstes folder as seen above as it will bloat your apk. Thanks.