Spans / Text

App UI / UX – Part 2

In the previous article we began improving the UI of the app we developed as part of the Bluetooth LE series. We updated the layout to something slightly more interesting, but there is still more work to do. In this article we’ll look at how to use custom typefaces within our app.

As is often the case in Android development, there are a number of ways of applying a custom typeface to a TextView. The most obvious is to subclass TextView, and override onDraw to apply your custom Typeface object. A much more elegant solution is to use Chris Jenkins’ Calligraphy library – Chris’ approach really takes the effort out of using custom typefaces, and is highly recommended for any serious project.

Rather than use Chris’ library, I’m going to take a different approach. The reason for this is that it is a great excuse to demonstrate the amazing power of Android Spans. Spans have been covered on Styling Android before and they are an incredibly powerful tool. I should add that Calligraphy includes a class named CalligraphyTypefaceSpan which is equivalent to the CustomTypefaceSpan class which we’ll create later on.

Firstly, let’s select the font that we’re going to use. I decided upon Chivo Black from Google Fonts. I chose a thick font because Jeff Gilfelt would get annoyed if I used a thin one. I chose to obtain it from Google Fonts because the fonts are open source.

After downloading Chivo-Black.ttf, copy it in the the assets folder within the project. We can then load it really easily:

Typeface textFont = Typeface.createFromAsset(
	getResources().getAssets(), 
	"Chivo-Black.ttf");

A quick look at the Android API docs might suggest that we’re going to use TypefaceSpan to utilise this Typeface object, but unfortunately TypefaceSpan only allows built in typefaces to be used. However, rolling our own Span implementation which allows us to use our custom Typeface is simplicity itself:

private static class CustomTypefaceSpan extends MetricAffectingSpan {
	private final Typeface mTypeface;

	public CustomTypefaceSpan(Typeface typeface) {
		mTypeface = typeface;
	}

	@Override
	public void updateMeasureState(TextPaint p) {
		p.setTypeface(mTypeface);
	}

	@Override
	public void updateDrawState(TextPaint tp) {
		tp.setTypeface(mTypeface);
	}
}

An obvious question is: Why don’t we extend TypefaceSpan? The simple answer is that TypefaceSpan has additional arguments to specify which of the built-in typefaces to use, and we simply don’t need them. We extent MetricAffectingSpan because it is implicit that the changes that the CustomTypefaceSpan will make to the text being displayed will alter the text metrics, and so these will be re-calculated after updateMeasureState() has been called, and the dimensions of the text that will be rendered will be calculated correctly.

All we need to do now is set the CustomTypefaceSpan on the text:

public class DisplayFragment extends Fragment {
	public static DisplayFragment newInstance() {
		return new DisplayFragment();
	}

	private TextView mTemperature = null;
	private TextView mHumidity = null;

	private CharacterStyle text = null;

	@Override
	public View onCreateView(LayoutInflater inflater, 
		ViewGroup container, Bundle savedInstanceState) {
		View v = inflater.inflate(R.layout.fragment_display, container, false);
		if (v != null) {
			mTemperature = (TextView) v.findViewById(R.id.temperature);
			mHumidity = (TextView) v.findViewById(R.id.humidity);
		}
		Typeface textFont = Typeface.createFromAsset(
			getResources().getAssets(), getString(R.string.text_font));
		text = new CustomTypefaceSpan(textFont);
		return v;
	}

	public void setData(float temperature, float humidity) {
		if (mTemperature != null) {
			SpannableStringBuilder ssb = new SpannableStringBuilder(
				getString(R.string.temp_format, temperature));
			ssb.setSpan(text, 0, ssb.length(), 
				Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
			mTemperature.setText(ssb);
		}
		if (mHumidity != null) {
			SpannableStringBuilder ssb = new SpannableStringBuilder(
				getString(R.string.humidity_format, humidity));
			ssb.setSpan(text, 0, ssb.length(), 
				Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
			mHumidity.setText(ssb);
		}
	}

	private static class CustomTypefaceSpan extends MetricAffectingSpan {
		private final Typeface mTypeface;

		public CustomTypefaceSpan(Typeface typeface) {
			mTypeface = typeface;
		}

		@Override
		public void updateMeasureState(TextPaint p) {
			p.setTypeface(mTypeface);
		}

		@Override
		public void updateDrawState(TextPaint tp) {
			tp.setTypeface(mTypeface);
		}
	}
} 

So, applying a custom font is really quite simple, but let’s not leave it just there, let’s also try and do something in place of the static TextView labels that we had originally. The values being displayed imply what they are because of the units that we’re displaying (“°C” for temperature, and “%” for humidity), but why don’t we add some nice symbols instead? We can use a nice symbol font to do this. I wasn’t able to find a symbol font on Google Fonts which suited the needs of the app, but it didn’t require much searching to find Ionicons, a free, open-source font which contains both a thermometer symbol and and a water droplet symbol.

Once again, we drop the font in to assets, and load it in to a Typeface object in exactly the same manner as before. A symbol font is a little different to a standard, character font because with a character font, the character mappings remain pretty much the same, but there is no standard for a thermometer or droplet icon, so we need to understand the character mappings of the typeface in order to use it. For ionicons, we visit the website, and click on the specific symbol that we’re interested in, and it will return the unicode mapping of the symbol. The thermometer is \F2B8, and the droplet is \F25B. To use these within a Java string we just use the unicode escape sequence, and these become \uF2B8 and \uF25B respectively.

In order to make things a little more visually interesting, we’ll also size these icons so that they are half the size of the rest of the text in the respective string. Once again, we can use a Span to do this, and RelativeSizeSpan will do what we require:

private CharacterStyle text = null;
private CharacterStyle symbol = null;
private CharacterStyle size = null;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
	View v = inflater.inflate(R.layout.fragment_display, container, false);
	if (v != null) {
		mTemperature = (TextView) v.findViewById(R.id.temperature);
		mHumidity = (TextView) v.findViewById(R.id.humidity);
	}
	Typeface textFont = Typeface.createFromAsset(
		getResources().getAssets(), getString(R.string.text_font));
	text = new CustomTypefaceSpan(textFont);
	Typeface symbolFont = Typeface.createFromAsset(
		getResources().getAssets(), getString(R.string.symbol_font));
	symbol = new CustomTypefaceSpan(symbolFont);
	size = new RelativeSizeSpan(0.5f);
	return v;
}

public void setData(float temperature, float humidity) {
	if (mTemperature != null) {
		SpannableStringBuilder ssb = new SpannableStringBuilder(
			getString(R.string.temp_format, temperature));
		ssb.setSpan(symbol, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		ssb.setSpan(size, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		ssb.setSpan(text, 1, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		mTemperature.setText(ssb);
	}
	if (mHumidity != null) {
		SpannableStringBuilder ssb = new SpannableStringBuilder(
			getString(R.string.humidity_format, humidity));
		ssb.setSpan(symbol, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		ssb.setSpan(size, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		ssb.setSpan(text, 1, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		mHumidity.setText(ssb);
	}
}

We also need some string definitions:

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<string name="app_name">BluetoothLE</string>
	<string name="action_refresh">Refresh</string>
	<string name="scanning">Looking for devices. Please wait…</string>
	<string name="no_devices">No devices found.</string>
	<string name="temp_format">\uf2b6 %1$.1f°C</string>
	<string name="humidity_format">\uf25b %1$.1f%%</string>

	<string name="symbol_font">ionicons.ttf</string>
	<string name="text_font">Chivo-Black.ttf</string>
</resources>

If we run this we can see our nice thick font for the numbers, plus the thermometer and droplet symbols:

display_font

Now that we have the text looking much nicer, what about the background? A blue background hardly matches the temperature of 27°C being displayed. In the next article we’ll turn our attention to the background.

The source code for this article can be found here.

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Hey Mark, Great article.
    ]
    We actually went down the Spannable route with upcoming 1.0 of Calligraphy, it gets around some odd limitations (and implementations) of TextView.

    1.0 will expose a set textview spannable “setCustomTypeFace(Spannable, Typeface)” Hopefully making it even less painful for some cases 🙂

    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.