Custom UI Elements in Android: Odometer Tutorial Part I

The finished Odometer

Introduction

This series of tutorials will provide an introduction to creating custom user interface elements in Android. By the end, you will have a complete custom widget that will allow input of numbers as if on an odometer. The user will slide the digits up and down, and change the digits.

This first part of the tutorial will get our test project created, and start building out the basic element of our odometer – a single-digit spinner.

Odometer Tutorial
  1. Part I: First Steps « You are here
  2. Part II: Touch Input and Scrolling Numbers
  3. Part III: Compound Widgets and Fixing Bugs

Odometer Tutorial Part I: First Steps

In this first step tutorial, we will get the foundation laid for our Odometer widget, and get through drawing the single-digit spinner, but we won’t get to interactivity yet.

Creating the Project

The first step is to create a new Android project in Eclipse. I used the following values:

Project Name: OdometerTutorial
Build Target: 2.1
Application Name: Odometer
Package Name: com.kdion.tutorial.odometer
Create Activity: OdometerTutorialMain
min sdk: 7

Settings for creating the project

Project Settings

(Special Note: if you look at the settings in the image, you will see that I mis-typed ‘tutorial’ as ‘turotial’ in the Package name. This came back to bite me later, so I had to fix it after I took this screenshot. Don’t make the same mistake as me!)

You can use whatever values you wish for these, but just be aware that my code is based on using these values so if you use different values (say, for the Package Name), your code will need to change accordingly.

With your newly created project, go ahead and run it to make sure everything is playing nice with the emulator (or your dev phone)

Screenshot of running the newly created project

Running the new project

OdometerSpinner

To begin our custom widget creation, we will first create the building blocks for the odometer. Our first class, OdometerSpinner, will represent a single digit in the final odometer. In case you aren’t familiar with the old non-digital odometers, each digit is on its own wheel/spinner that goes from 0 to 9 and wraps at 10. So as you are driving along, the digits on a spinner go …8-9-0-1… When a spinner goes from 9 to 0, the next spinner increments as well, but we won’t be implementing that behavior, since it would be a pain to input numbers if our widget worked that way. Also, be aware that I use the term ‘Spinner’ in a different context than the standard Android ‘Spinner’ UI class, which is more of a drop-down (or at least that’s how I think of it).

Create the OdometerSpinner class

Add a new class to your project, using the settings shown in the image below. You want to inherit from View, and I go ahead and have it auto-generate the default constructors for me, as well as any abstract methods (although there aren’t any when inheriting form View). I also have ‘Generate comments’ checked, although you don’t have to. The important thing is to make a class names ‘OdometerSpinner’ that subclasses android.view.View.

OdometerClass settings

Creation settings for OdometerClass

Once you create the class, you will see that there are three different constructors generated for you, looking something like this:

public OdometerSpinner(Context context)
{
    super(context);
}

public OdometerSpinner(Context context, AttributeSet attrs)
{
    super(context, attrs);
}

public OdometerSpinner(Context context, AttributeSet attrs, int defStyle)
{
    super(context, attrs, defStyle);
}

These are the three View constructors, and it is important you override them all in your custom classes. The first constructor is the one called when you create the class in code. The second and third come into play when your custom View is created from an xml file. The AttributeSet passed is the set of xml attributes defined – the defStyle in the third constructor has to do with applying a custom theme – I have never used it myself, but I figure it is best to override the constructor just in case.

onMeasure() and onDraw()

The next methods you will need to override in OdometerSpinner are onMeasure and onDraw. So go ahead and add them through Source > Override/Implement Methods… and select onMeasure(int, int) and onDraw(Canvas). It is recommended that you override these methods whenever you are making your own custom View, so while we won’t do much with them right now, we’ll go ahead and get them in place. They should look something like this (comments are mine):

protected void onDraw(Canvas canvas)
{
    super.onDraw(canvas);

    // TODO draw stuff
}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // MAKE SURE you call super.onMeasure(int, int) here!!!
    // At least until we implement our own measuring logic
}

Adding OdometerSpinner to the Activity

The next step is to add the OdometerSpinner to our Activity’s layout so we can see it when we run. Open up main.xml in the layout/ folder and add OdometerSpinner so it looks like this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
	<TextView
	    android:layout_width="fill_parent"
	    android:layout_height="wrap_content"
	    android:text="@string/hello"
	    />
	<com.kdion.tutorial.odometer.OdometerSpinner
	   android:id="@+id/single_spinner"
	   android:layout_width="fill_parent"
	   android:layout_height="fill_parent"
	   />
</LinearLayout>

Your syntax for OdometerSpinner might not be ‘com.kdion.tutorial.odometer.OdometerSpinner‘, just change the package path to whatever you are using for your package. The important thing to note is to set the layout_width and layout_height to fill_parent (this has to do with not overriding the logic in onMeasure(), so until we put logic in there leave the width and height as fill_parent). I also just kept the default TextView with the “Hello, World!” message for grins, but you don’t have to.

Now, go ahead and run your app once again…but wait! Nothing shows up for our spinner! It is still there, we just haven’t yet drawn anything – so it is invisible. We will start drawing next.

Drawing the Background

The first thing we will do for drawing the background is to simply draw a solid rectangle to represent our OdometerSpinner. The simplest way to do this is to add the following drawing commands to OdometerSpinner’s onDraw() method:

protected void onDraw(Canvas canvas)
{
	super.onDraw(canvas);

	int width = getMeasuredWidth();
	int height = getMeasuredHeight();

	Paint p = new Paint();
	p.setColor(Color.GREEN);

	canvas.drawRect(0, 0, width, height, p);
}

First, we get the width and height of our View by calling getMeasuredWidth() and getMeasuredHeight(). Next, we create a Paint object and set it a green color. Finally, we call drawRect() on the Canvas object passed to us. The first two zeros passed as arguments to the drawRect() function define the top left corner of the rectangle (if you are unfamiliar with computer graphics, the coordinate system used by most drawing packages has the origin at the top left corner of the screen, with increasing X-values going to the right and increasing Y-values going down).

Now, if we run our app, we see that there is now a nice big green rectangle taking up most of the screen.

The app running with a bright green background

Running our app with a green background

Improving the drawing

Before we move on to changing up the background to look more like an odometer, we need to change how we are drawing the background. As we have the code now, we query the width and height of our View, create a Paint object, and draw the rectangle. This is horribly inefficient, since onDraw() might be getting called many times a second (at least once we start having scrolling numbers and such). So what we need to do is move as much of the overhead as possible out of onDraw(). To do that, we will add new class member variables for the width, height, background rectangle, and background paint. In general, it is considered ‘best practice’ in Android to move things to class members rather than re-calculate within the methods every time they are called. So, add the following variables to the OdometerSpinner class:

public class OdometerSpinner extends View
{
	private float mWidth;
	private float mHeight;
	
	private RectF mBGRect;
	
	private Paint mBGPaint;
	//...

Now that we have those new variables, we need to initialize the Paint and RectF when the class is instantiated. So add the following initialize() method to OdometerSpinner, and add calls to initialize() in each of the 3 constructors.

private void initialize()
{
	mBGRect = new RectF();
	
	mBGPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	mBGPaint.setColor(Color.GREEN);
}

Now that we have the Paint saved in our class, we need to store the width and height of our widget. Instead of measuring the width and height every time we draw, we really only need to change our stored measurements when the size of the View changes. And wouldn’t you know it, there is a method you can override in View that is called every time the size changes – onSizeChanged(). Add an override for onSizeChanged in OdometerSpinner and put the following in it:

protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
	super.onSizeChanged(w, h, oldw, oldh);
	
	mWidth = w;
	mHeight = h;
	
	mBGRect.set(0, 0, w, h);
}

Here we set the width and height, and resize our saved rectangle to the new size – now we don’t need to perform the cumbersome recalculation every time we draw. Now just change the onDraw() code:

protected void onDraw(Canvas canvas)
{
	super.onDraw(canvas);
	
	canvas.drawRect(mBGRect, mBGPaint);
}

Much nicer, isn’t it? You can go ahead and run and see that there is stil that big green screen.

Drawing Gradients

Now it is time to get rid of that bright green and replace it with a more realistic-looking gradient for the background. Using a gradient for the background will give us the illusion of depth on out spinner and make it look like we are showing part of a round object – just what we want.

At the top of OdometerSpinner, add a new member variable for this GradientDrawable, and add its initialization in initialize(). You can remove the Paint and RectF we were using before. Also, add the resizing logic in onSizeChanged(), and draw the gradient in onDraw().

private GradientDrawable mBGGrad;
//...
private void initialize()
{
	mBGGrad = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, 
				new int[] { 0xFF000000, 0xFFAAAAAA, 0xFF000000 });
}
//...
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
	super.onSizeChanged(w, h, oldw, oldh);
	
	mWidth = w;
	mHeight = h;

        mBGGrad.setBounds(0, 0, w, h);
}
//...
protected void onDraw(Canvas canvas)
{
	super.onDraw(canvas);
	
	mBGGrad.draw(canvas);
}

In initialize() we create a new GradientDrawable, setting the direction to go from top to bottom. We also pass in an array of ints, which are the ordered colors we want in the gradient. We tell the gradient to have black at the top, fade to a medium gray in the middle, and back to black. The format of the ints we pass is 0xAARRGGBB, where AA is the alpha channel, and RR, GG, and BB are the red, green, and blue channels, respectively.

In onSizeChanged(), we tell the gradient its new dimensions. In onDraw(), we tell it to draw itself on the canvas. Now when you run the app, you should get a nice vertical gradient as shown below.

Our app running with a gradient background

Our OdometerSpinner with the gradient background

Throwing Numbers Around

Now that we have a nice looking background, we need to have our odometer actually display some numbers. Rather than worry about all of the logic for changing numbers, having the values wrap, and so on – for now, we’ll just draw a single, hard-coded digit.

Canvas has a nice method to draw a string, so we will need a String to hold the text representation of our digit (again, rather than convert int to String in every onDraw() call, we store it as a separate variable). We will also need a Paint to control how the text looks, so we will add those variables. We will initialize them along with our other class members in initialize(), then draw them in onDraw().

private String mDigitString;
private Paint mDigitPaint;
//...
private void initialize()
{
	mBGGrad = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, 
			new int[] { 0xFF000000, 0xFFAAAAAA, 0xFF000000 });

	mDigitString = "8";
	
	mDigitPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	mDigitPaint.setColor(Color.WHITE);
	mDigitPaint.setTextAlign(Align.CENTER);
}	
//...
protected void onDraw(Canvas canvas)
{
	super.onDraw(canvas);
	
	mBGGrad.draw(canvas);
	
	float digitX = mWidth / 2;
	float digitY = mHeight / 2;
	
	canvas.drawText(mDigitString, digitX, digitY, mDigitPaint);
}

I went ahead and used 8 as the digit we will display for now, since it is pretty easy to tell if it is centered on the screen when it is displayed. We also set up the Paint we use to draw the digit, telling it to center the text and color it white. In onDraw(), we draw the digit string in the center of the OdometerSpinner with the appropriate Paint. Running our app at this point gives us the following:

The OdometerSpinner with a tiny number in the middle

Drawing a tiny number

Sizing

As you can see, the text is very tiny, because Android by uses the default text size for the Paint since we did not specify a size. Since the size we want the digits to be will depend on the size of the spinner itself, we can put the following line of code in the onSizeChanged() call to set a proper text size.

protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
	//... other code here...
	mDigitPaint.setTextSize(h);
}

This sets the text height to be the height of the spinner, which should fill up the spinner with the digit, which is what we want. Running now gives us this:

A large 8 drawn on the screen, but clipped at the top

Big number, bad placement

Placement

The text is now big enough, but it is not placed correctly. In onDraw(), when we passed the Y-value to draw as mHeight/2, that actually specifies the bottom of the text. We can pass mWidth/2 for the X-value and get it centered horizontally because Paint has a text alignment that we set as CENTER. In order to get our text centered vertically, we are going to have to perform a bit more calculation, measuring how tall the text we want to display is, and using that measurement to calculate where we need to place it to get it perfectly centered. Unfortunately, every digit (0-9) might have different heights, so we don’t want to use a single, hard-coded value. Fortunately, there are some built-in functions we can use to get the exact height of what we want to draw. Add two new variables, mDigitX and mDigitY to store the location we want to draw the digit, and add the following code to onSizeChanged() and onDraw()

private float mDigitX;
private float mDigitY;
//...
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
	super.onSizeChanged(w, h, oldw, oldh);
	
	mWidth = w;
	mHeight = h;
	
	mBGGrad.setBounds(0, 0, w, h);
	
	mDigitPaint.setTextSize(h);
	
	Rect bounds = new Rect();
	mDigitPaint.getTextBounds(mDigitString, 0, 1, bounds);
	
	int textHeight = Math.abs(bounds.height());
	
	mDigitX = mWidth / 2;
	mDigitY = mHeight - ((mHeight - textHeight) / 2);
}
//...
protected void onDraw(Canvas canvas)
{
	super.onDraw(canvas);
	
	mBGGrad.draw(canvas);
	
	canvas.drawText(mDigitString, mDigitX, mDigitY, mDigitPaint);
}

In onSizeChanged(), we use getTextBounds() to measure the smallest rectangle that would fit around mDigitString (characters 0 to 1), putting those dimensions in the rectangle bounds. Then, we use the height of that rectangle (taking the absolute value since it isn’t guaranteed to be positive, and a negative number would mess with our calculations) to calculate exactly where to place the digit when we draw it. With the appropriate changes to the arguments in onDraw(), we now get the following when we run our app:

The large number drawn centered on the screen

Correctly placed, correctly sized number

That is all for Part I of this tutorial, in Part II we will add touch interaction and the ability to change numbers with our spinner.

Source

Here is the source code for Part I. (.zip file, Right-click > Save As…)

Odometer Tutorial
  1. Part I: First Steps « You are here
  2. Part II: Touch Input and Scrolling Numbers
  3. Part III: Compound Widgets and Fixing Bugs

6 Comments

  1. Frisps
    April 24, 2011 at 2:14 pm |

    Hi,

    I’m kinda stuck with your code. I get follwing error and can’t solve it out:

    at main.xml: java.lang.UnsupportedOperationException

    which is: mDigitPaint.getTextBounds(mDigitString, 0, 1, bounds);

    I tried your source code as well, and get the same message, maybe theres a settings issue?

    Thank you very much for your help!

  2. June 4, 2011 at 3:51 am |

    really great tutorial. thanks a lot. i learned how to create a custom ui from you

  3. Anamika
    July 9, 2012 at 4:25 pm |

    Thanks a lot Kevin. Really great tutorial on creating custom views in android. Extremely detailed and very easy to follow each step.

  4. Anish Hegde
    July 10, 2012 at 12:32 pm |

    Thanks for such a wonderful tutorial . Looking forward for more of these .

  5. July 19, 2012 at 7:03 am |

    Note that calling:
    int width = getMeasuredWidth();
    int height = getMeasuredHeight();

    is wrong
    you should be calling:
    int width = getWidth();
    int height = getHeight();

    in the onDraw() method and, generally, outside the onLayout / onMeasure method.

    And manually draw a background is a bad Idea :) you should provide a default one and let the parent View draw it but let the user override it..

    Obviously it serve as an example and I think you wrote a good tutorial ;)

  6. December 30, 2012 at 3:51 pm |

    It’s awesome in support of me to have a web site, which is helpful in favor of my know-how. thanks admin


© 2010 Kevin Dion

Switch to our mobile site