Yet another way for applying fonts … and more :)

Andras Nemeth
5 min readMay 29, 2016

--

If you are an Android developer I’m guessing you have came to a point when you had/wanted to change font’s for items in you layouts. Though this is not rocket science, the framework — or AppCompat for that matter — doesn’t give you an out of the box solution if you want to do it in a declarative fashion.

Loading font’s is pretty straight forwards, independently from what the source is, and setting them is a no brainer either. But setting fronts from code is not an elegant solution especially if you have to change them later, and fonts are set all around your code base. Also if you have a designer who is willing and able to fiddle with layouts, it makes their life much harder.

The widely used approach — in my experience — is subclassing all — or only required — TextView derived classes and using a custom attribute to declare the font to be used. This is a step in the right direction but it’s tedious plus you end up with a number of classes that don’t add much. Also consider this:

Say you have subclassed the framework classes like TextView, Button, etc. What happens with the features the were back ported through AppCompat ?

So the question is:

Is there a way to apply fonts without subclassing framework or AppCompat view classes (or to apply any custom properties that would not necessarily require subclassing) ?

Before I give the answer let’s look at a bit how layout inflation is done and than just glance at how AppCompat utilises this to bring you all those back ported goodies.

The LayoutInflater

I’m not going to go in too much details on this matter. If you are really interested: “use the source” !

Let’s take the simplest case: you are inflating a layout through

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

At this point the LayoutInflater will start parsing the layout resource passed. Once it finds a start tag, it

  1. Creates view from the tag
  2. Recursively inflates child view
  3. Returns the root view

Up until this point it seems that LayoutInflater does not five you much in the way of customisation. That is where android.view.LayoutInflater.Factory and android.view.LayoutInflater.Factory2 come into play. Version 2 extends version 1 and adds an extra methods that supplies the parent view for the view to be inflated. For simplicity let’s only consider version 1.

public interface Factory {
public View onCreateView(String name,
Context context,
AttributeSet attrs);
}

Whenever LayoutInflater comes across a new tag (if a factory has been set) it gives the factory a chance to create the view. If the factory returns null from onCreateView the LayoutInflater goes ahead and tries to inflate the view itself.

This raises a question: how does for example the LayoutInflater inflate android.widget.TextView when you usually just type something like this:

<TextView
android:text=”@string/hello_world
android:layout_width=”match_parent”
android:layout_height=”wrap_content” />

For this the LayoutInflater uses a set of prefixes to search for the class to be inflated. The base class only uses “android.view”, but for example the AOSP declared PhoneLayoutInflater extends the list to use

private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};

as well.

AppCompat

If you look at the source of AppCompatActivty in onCreate you’ll find something like this

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}

First an AppCompatDelegate is “constructed” and installViewFactory is invoked. installViewFactory eventually sets the delegate as a Factory on the LayoutInflater of the current context (if it can, as only one factory can be set on a LayoutInflater …). So now whenever a layout is inflated from resource the factory is invoked to create the view. This is how AppCompat intercepts for example TextView creation and instead of returning an instance of the framework’s provided TextView, it returns an instance of AppCompatTextView. The actual view creation is implemented by AppCompatViewInflater.

The solution

Through the use of of layout factories we can inflate our own view instead of framework versions, or inflate any view for our custom used tags names. This however doesn’t get us what we want. What we want is this:

After a view has been constructed we would want a reference to the view plus it’s attributes so that we can get our custom attributes and apply whatever we want to apply on the view(s). Factories (and LayoutInflater for that matter) doesn’t allow this out of the box, so a little trickery is required.

There are two cases to cover

  1. AppCompat
  2. Framework

The AppCompat cases is a bit easier. The framework case is not much more complicated but unfortunately requires just a bit of reflection but the overall idea is the same.

In both cases the general idea is to install our own LayoutInflater.Factory but not doing the actual view creation. Once our own factory has been installed, each time a new view is to be created we let AppCompat/Framework construct the view and then execute our own logic to apply to the newly constructed view whatever is that we want to apply (e.g.: custom fonts).

AppCompat Solution

Since only one view factory can be installed at a time we need to prevent the AppCompat version to be installed. To do this, before AppCompatActivity.onCreate is called we

  1. Fetch the AppCompatDelegate
  2. Install our own factory that, propageates the call to the AppCompat view factory then invokes our own logic.

Framework solution

The framework solution is pretty much the same with the exception that there is no factory that we can wrap. Instead we want to wrap the calls to the LayoutInflater view creation itself. To achieve this we install our factory which will call back into the LayoutInflater to create the view itself and then call our own logic. The problem here is the visibility of the methods that implement the actual view creation, so sadly this is where a bit of reflection comes into play.

Static layout

Applying fonts

So how to apply custom fonts using the the concept presented above ? In the most simplistic scenario it only requires just a few lines of code.

First you need to declare your styleable attributes. How you do this is you choice. You can declare an enum or just have a string attribute that references a font file on your assets folder. (For demo purposes I have used enums, but take you pick ;) )

<?xml version=”1.0" encoding=”utf-8"?>
<resources>
<declare-styleable name=”Font”>
<attr format=”enum” name=”font”>
<enum name=”AnonymousProRegular” value=”0" />
<enum name=”AvroRegular” value=”1" />
<enum name=”ChivoRegular” value=”2" />
<enum name=”VarelaRoundRegular” value=”3" />
</attr>
</declare-styleable>
</resources>

In you Activity’s onCreate lifecycle method install the ViewDecorator using the ViewDectoratorInstaller like so

@Override   
protected void onCreate(Bundle savedInstanceState) {
installViewDecorator();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
}
void installViewDecorator() {
ViewDecoratorInstaller installer =
new ViewDecoratorInstaller(this);
installer.install(this);
}

Implement the ViewDecorator interface and apply the font.

@Override 
public void decorate(View parent, View view, Context context,
AttributeSet attrs) {
if(view instanceof TextView) {
TextView textView = (TextView) view;
textView.setTypeface(getTypeFace(context, attrs));
}
}
Typeface getTypeFace(Context context, AttributeSet attrs) {
Typeface typeFace = null;
TypedArray ta = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.Font, 0, 0);
int ordinal = ta.getInt(R.styleable.Font_font, -1);
if(ordinal != -1) {
Font font = Font.values()[ordinal];
typeFace = font.getTypeFace(getAssets());
}
ta.recycle();
return typeFace;
}

For more details check out the demo on github, where the full implementation is available for the view decorator concept as well.

--

--