Usually on Styling Android the focus is on individual, standalone features and techniques for Android UI / UX development. In this series we’re going to create a very simple app which will be published to Google Play. While we will certainly cover some features and techniques that we haven’t looked at before, as well as some new features in Android 4.2, we’ll also look at some of the design decisions required to get even the simplest of apps ready for market.
So what’s this app that we’re going to create? It is a simple clock widget which displays the time using words rather than numbers. The idea came from one of the custom watch faces for the Pebble, but we’ll give it an Android twist by giving it a Holo look and feel. The initial version of the app will be a simple App Widget which the user can install on their home screen:
As we progress with the series, the version of Text Clock released to Google Play will be updated to match the latest published article.
The first think that we need to do is to create an Android 4.2 project named “TextClock” with the package name com.stylingandroid.textclock, and with no Activity – we’ll add one later, but we have no need for one now.
Next we’ll look at the main class that will generate the time in words:
public class TimeToWords { public static final String[] UNITS = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; public static final String[] TENS = { "zero", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" }; public static final String[] TEENS = { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" }; private static final StringBuilder builder = new StringBuilder(); public synchronized static String[] timeToWords( Calendar date ) { builder.setLength( 0 ); int h = date.get( Calendar.HOUR_OF_DAY ); int m = date.get( Calendar.MINUTE ); if ( h == 0 ) { builder.append( "midnight" ); } else { toWords( h, builder, false, " " ); } if ( m == 0 ) { if ( h > 0 ) { builder.append( ":o'clock" ); } } else { builder.append( ":" ); toWords( m, builder, true, ":" ); } return builder.toString().split( ":" ); } private static void toWords( final int number, StringBuilder sb, boolean leadingZero, String separator ) { int num = number; int tens = num / 10; if ( leadingZero || tens > 0 ) { if ( tens == 1 ) { sb.append( TEENS[num - 10] ); num = 0; } else { sb.append( TENS[tens] ); } } int units = num % 10; if ( units > 0 ) { if ( sb.length() > 0 ) { sb.append( separator ); } sb.append( UNITS[units] ); } } }
There are a few things worth explaining here. Firstly, the methods are static, so we never need to instantiate a TimeToWords object, but it utilises a static StringBuilder member. The reason for this is to minimise the number of objects which are created when the timeToWords()
method is called. There is one potential problem with this approach: If timeToWords()
is called by different threads simultaneously, the use of a single StringBuilder will cause problems. Therefore we declare timeToWords()
as synchronized
to ensure that it can only have one active invocation. Hopefully the rest of the app design should mean that this is unnecessary, but it’s always worth considering thread safety issues, just in case things don’t get implemented in quite the way we expect at this stage.
At this point we’ll just focus on generating a 24 hour clock representation of the time, but we’ll change to optionally support a 12 hour representation later. What happens here is that we’ll generate a text string containing the time in words, and then split it into components which get returned as a String array. We use the separator “:” to indicate where we want to break. We only have three lines on the display, and in some circumstances there will be more than three distinct words (such as “twenty one fifty five”). In such cases we want to displays the hours on one line (“twenty one”), and the minutes on the other two lines (“fifty” and “five”). We also have some edge cases for 00 hours (where we want to display “midnight”), and when the time is 0 minutes past the hour (when we want to display “o’clock” instead of the minutes).
That’s the main “engine” of our app which performs all of the backend business logic. It’s pretty simple so that we can devote the remainder of this series to the UI – how we present this to the user.
In the next article we’ll start on this presentation by beginning to implement an app widget which the user can add to the home screen of their device.
The source code for this article can be found here, and TextClock is available from Google Play.
© 2013 – 2014, Mark Allison. All rights reserved.
Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Hmmm, I think midnight is 0:00 exactly, and 0:30 is not midnight.
So what should it be, then?
Twelve Thirty
Really, when using the 24-hour clock?
>There is one potential problem with this approach: If timeToWords() is called by different threads simultaneously, the use of a single StringBuilder will cause problems<
Then why not use ThreadLocal?
You could use ThreadLocal but it could potentially lead to additional object creation. But as I mention in the article I really don’t expect this method to be called by different threads, and it’s pretty lightweight, so won’t block significantly because of the synchronized method even if it did.
Really? I’m using ThreadLocal in many places on my apps, to prevent slowness because of “synchronized” keyword but still preventing race problems. Does using ThreadLocal cause many unnecessary object allocations?
It depends on the code in question. But as I said, for this code it really is unnecessary and synchronized gives precisely the thread safety that I need.
What about using strings.xml in the resources? That way you can easily add support to different languages 🙂
I thought of that but there are different syntax rules for constructing the time in different languages so it would require different logic for different languages, not just the string translations. For that reason I decided to stick to English only.