Android UI Tutorial – Odometer Part III: Building the Odometer

[This is the third part of the tutortial so far. If you haven’t already, check out Part 1 and Part 2 to get up to speed on where we are. You can also grab the source code we ended up with at the end of Part 2 to follow along.]

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

Creating the Odometer Widget

Now that we have our single digit spinner, we can create the actual odometer. Create a new class in the project, and name it Odometer. Instead of View, subclass from TableLayout. We will use the existing capabilities of TableLayout to automatically divide our widget into equal pieces for each spinner.

The class creation dialog for Odometer
Odometer class creation

To define how our widget will display, we will use an xml layout file. Use the Android XML file wizard to add a new layout file as shown below.

The xml file creation dialog for the Odometer layout file
Odometer layout file creation

Name the file widget_odometer.xml, and make sure to change the root element to TableLayout. Since this is going to be the catch-all layout for the widget, don’t select any qualifiers from the list. Once the file is created, open up the xml view (if it opens in the graphical view), and add the following to the <TableLayout> element:

<TableLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:stretchColumns="*"
    >
    <TableRow>
        <com.kdion.tutorial.odometer.OdometerSpinner
            android:id="@+id/widget_odometer_spinner_100k"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
        <com.kdion.tutorial.odometer.OdometerSpinner
            android:id="@+id/widget_odometer_spinner_10k"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
        <com.kdion.tutorial.odometer.OdometerSpinner
            android:id="@+id/widget_odometer_spinner_1k"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
        <com.kdion.tutorial.odometer.OdometerSpinner
            android:id="@+id/widget_odometer_spinner_100"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
        <com.kdion.tutorial.odometer.OdometerSpinner
            android:id="@+id/widget_odometer_spinner_10"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
        <com.kdion.tutorial.odometer.OdometerSpinner
            android:id="@+id/widget_odometer_spinner_1"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
    </TableRow>
</TableLayout>

We now have the definition for a single row of 6 OdometerSpinners. The android:stretchColumns="*" tells the layout to size each of the columns (our spinners) the same. This saves us from needing to do that ourselves. We also set ids for each spinner, with names that help us keep track of which spinner is which digit (1s place, 10s place, etc).

Next, we need to tell our Odometer to use this layout file to draw itself. In the Odometer class, add an initialize() method like we did for OdometerSpinner where we can do our initialization independently regardless of which constructor is called. Add the following code:

private static final int NUM_DIGITS = 6;
private OdometerSpinner[] mDigitSpinners;
//...
private void initialize()
{
	mDigitSpinners = new OdometerSpinner[NUM_DIGITS];
	
	// Inflate the view from the layout resource.
	String infService = Context.LAYOUT_INFLATER_SERVICE;
	LayoutInflater li;
	li = (LayoutInflater)getContext().getSystemService(infService);
	li.inflate(R.layout.widget_odometer, this, true);
	
	mDigitSpinners[0] = (OdometerSpinner) 
		findViewById(R.id.widget_odometer_spinner_1);
	mDigitSpinners[1] = (OdometerSpinner) 
		findViewById(R.id.widget_odometer_spinner_10);
	mDigitSpinners[2] = (OdometerSpinner) 
		findViewById(R.id.widget_odometer_spinner_100);
	mDigitSpinners[3] = (OdometerSpinner) 
		findViewById(R.id.widget_odometer_spinner_1k);
	mDigitSpinners[4] = (OdometerSpinner) 
		findViewById(R.id.widget_odometer_spinner_10k);
	mDigitSpinners[5] = (OdometerSpinner) 
		findViewById(R.id.widget_odometer_spinner_100k);
	
}

The first thing we do is to use a LayoutInflater to ‘inflate’ the Odometer into the xml file we created. If you want to learn more about what exactly LayoutInflater does or other options you can use with it, check out LayoutInflater at Android Developers. The other thing we do in initialize() is to get references to each of the spinners and store them in an array so we can use them later.

Next, we need to change the main Activity layout so that instead of the single OdometerSpinner, we see our Odometer. In main.xml, remove the TextView with the ‘hello, world’ message and change OdometerSpinner to Odometer. The layout should now look 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"
    >
	<com.kdion.tutorial.odometer.Odometer
	   android:id="@+id/odometer"
	   android:layout_width="fill_parent"
	   android:layout_height="fill_parent"
	   />
</LinearLayout>

Now, run the app and see our great new Odometer!

Or maybe not – things don’t look too good.

The first view of the Odometer widget, the numbers are way too big
Our Odometer needs work

It looks a little better if you rotate the screen (Ctrl+F12 in the emulator), but the numbers in the spinners are way too big. You can still drag numbers up and down to change them, however – so at least that is good.

The odometer looks slightly better in landscape view
The numbers are more visible in landscape

Measurement and Sizing

In order for our view to draw and size properly, we need more robust logic than we have currently. At the moment, we are filling up the screen with the odometer (set with aandroid:layout_height="fill_parent" in the <Odometer> element in main.xml), but we probably don’t really want a tall and skinny odometer. What we really want is for the widest widget possible, and then let the Odometer determine how tall it should be based on that. Change main.xml to this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
	<com.kdion.tutorial.odometer.Odometer
	   android:id="@+id/odometer"
	   android:layout_width="fill_parent"
	   android:layout_height="wrap_content"
	   />
</LinearLayout>

Now if you ran the app, you wouldn’t see anything because android is relying on our code to know how tall to make the widget, but since we haven’t done anything with that it has a height of 0. So let’s fix that.

OdometerSpinner

In OdometerSpinner, we are going to add some logic to onMeasure() so our spinners get sized correctly.

public static final float IDEAL_ASPECT_RATIO = 1.5f;
//...
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	
	// get width and height size and mode
	int wSpec = MeasureSpec.getSize(widthMeasureSpec);
	int wMode = MeasureSpec.getMode(widthMeasureSpec);
	
	int hSpec = MeasureSpec.getSize(heightMeasureSpec);
	int hMode = MeasureSpec.getMode(heightMeasureSpec);
	
	int width = wSpec;
	int height = hSpec;
	
	// ideal height for the number display
	int idealHeight = (int) (wSpec * IDEAL_ASPECT_RATIO);
	
	if(idealHeight < hSpec)
	{
		height = idealHeight;
	}
	
	setMeasuredDimension(width, height);
}

The two arguments passed to onMeasure are encoded ints that contain both size and ‘mode’ information. To get the useful values, we need to call MeasureSpec.getSize() and MeasureSpec.getMode() on the passed width and height ints. The mode for height and width will be one of UNSPECIFIED, AT_LEAST, or EXACTLY, which lets us know how our parent wants us to size ourselves – at most n pixels, exactly n pixels, or unspecified (where the view can be any size it wants).

In this simple example, we don’t worry about the mode – we know that in our app we are constrained by width and need to set our height – but in an actual production widget you would want to account for the possible combinations of modes. For instance, how should our spinner calculate its size if it has to be short but can be as wide as it wants? But we won’t worry about that, because by setting all of our widgets to layout_width="fill_parent" in the layouts we get the end result we want (but in a somewhat ‘hacked’ manner).

In our new onMeasure(), we use a pre-defined aspect ratio we want to have for our spinner to calculate what our ideal height would be based on the width we are passed. Then, if that ideal height is less than the height we are passed, we use that one instead. This will fix the super tall numbers problem. Feel free to experiment with different values for the the aspect ratio, but I have found 1.5-1.6 to be pretty good for the default font android uses.

Odometer

We also need to add an implementation for onMeasure() to Odometer, so that it knows how tall to make itself.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	
	// get width and height size and mode
	int wSpec = MeasureSpec.getSize(widthMeasureSpec);
	int wMode = MeasureSpec.getMode(widthMeasureSpec);
	
	int hSpec = MeasureSpec.getSize(heightMeasureSpec);
	int hMode = MeasureSpec.getMode(heightMeasureSpec);
	
	// calculate max height from width
	float contentHeight = ((float)wSpec / NUM_DIGITS) 
			* OdometerSpinner.IDEAL_ASPECT_RATIO;
	
	int maxHeight = (int)Math.ceil(contentHeight);
	
	int width = wSpec;
	int height = hSpec;
	
	if(maxHeight < hSpec)
	{
		height = maxHeight;
	}
	
	setMeasuredDimension(width, height);
}

Nothing too complex here, we do the same calculation based on the aspect ratio, only we use 1/6th the width passed to us, since that is the value the spinners will be getting for their width.

Now, if we run our app, we get a much better looking odometer.

A view of the correctly sized odometer in portrait layout
The Odometer now sizes correctly for portrait view
A view of the correctly sized odometer in landscape view
Landscape gives us a larger widget

Much better! However, as you rotated the screen from portrait to landscape and moved the digits around, you probably noticed that every time you rotate the screen, all of the spinners reset to zero! This is due to the fact that every time the configuration of the device changes (such as going from portrait to landscape), the Activity is actually destroyed and restarted.

From the Activity documentation:

If the configuration of the device (as defined by the Resources.Configuration class) changes, then anything displaying a user interface will need to update to match that configuration. Because Activity is the primary mechanism for interacting with the user, it includes special support for handling configuration changes.

Unless you specify otherwise, a configuration change (such as a change in screen orientation, language, input devices, etc) will cause your current activity to be destroyed, going through the normal activity lifecycle process of onPause(), onStop(), and onDestroy() as appropriate.

Every time you rotate the screen, the app is killed and restarted – so all of the digits go back to zero (the initial default value).

Handling Activity Lifecycle Events

In order to maintain the value of the odometer when the screen orientation is changed (or when the user leaves the app and comes back), we will use some of the provided methods of Activity to store the value of the odometer before it is destroyed by the configuration change process, and then reading that value back and setting the odometer value accordingly. Of course, in order to do this we need to have a way to read and set the value of our Odometer.

Getting and Setting the value of OdometerSpinner

OdometerSpinner already has a method to set the digit in setCurrentDigit(int), so we just need to add a way to read the value to store later. So let’s go ahead and add one:

public int getCurrentDigit()
{
	return mCurrentDigit;
}

Nice and simple.

Get and Set in Odometer

Now we need to add code to get and set the value of the odometer.[Thanks to commenter Ted Hopp for giving me a better way to get and set the value of the individual spinners!]

private int mCurrentValue;
//...
public int getValue()
{
	int value = 0;
		
	for(int i = NUM_DIGITS - 1; i >= 0; --i)
	{
		value = 10 * value + mDigitSpinners[i].getCurrentDigit();
	}
	
	mCurrentValue = value;
	
	return mCurrentValue;
}

public void setValue(int value)
{
	for(int i = 0; i < NUM_DIGITS; ++i)
	{
		mDigitSpinners[i].setCurrentDigit(value % 10);
		value /= 10;
	}
}

In the get method, we read the value of each spinner and multiply it by the proper factor based on its position, adding the results to get the value represented by the whole. It is important that in initialize() you stored the spinners in increasing place order, so that the digit in the ones place it at index zero in the array – otherwise you will get incorrect values from this get call.

The set method is mostly the inverse of the get method, taking in the desired value and parsing out each digit. I didn’t put in a range check because in this case the only way to set the value is through the odometer itself (so no out of range data), but in a more widely used widget you would probably want to put some sort of check here.

Saving and Reading State

Now that we have a way to get and set the odometer value, let’s add the code in the main activity to handle the lifecycle. Add a class member for the Odometer, and add an override of onSaveInstanceState(Bundle), which is where we will add our code to store the value of the odometer. We will read the saved value in onCreate().

private static final String KEY_VALUE = "com.kdion.tutorial.odometer.OdometerValue";

private Odometer mOdometer;

private int mOdometerValue;
//...
public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	
	mOdometer = (Odometer) findViewById(R.id.odometer);
	
	if(savedInstanceState != null)
	{
		mOdometerValue = savedInstanceState.getInt(KEY_VALUE);
		mOdometer.setValue(mOdometerValue);
	}
}

protected void onSaveInstanceState(Bundle outState)
{
	super.onSaveInstanceState(outState);
	
	mOdometerValue = mOdometer.getValue();
	outState.putInt(KEY_VALUE, mOdometerValue);
}

We create a unique string key that we will use to store our data – in this case I used “com.kdion.tutorial.odometer.OdometerValue“, following the Java namespace format – but you could use any string you wish. In onSaveInstanceState(), we read the odometer value, then put that value in the Bundle we are passed. From the Android Docs for saveInstanceState(Bundle):

Called to retrieve per-instance state from an activity before being killed so that the state can be restored in onCreate(Bundle) or onRestoreInstanceState(Bundle) (the Bundle populated by this method will be passed to both)

So the Bundle we saved our odometer value to in onSaveInstanceState() is passed to the onCreate() method when it is being used to continue execution of our app (like when the screen orientation changes). So we read the vale from the Bundle, and use it to set the odometer’s value. If there is not a value stored in the bundle (such as if this were a clean run of the app where there was no previous state), we don’t do anything.

There are a lot of different methods that you can (and should) use to save and restore your app’s state that deal with the Activity lifecycle. They all have slightly different uses and times they are better to be used over others. If you aren’t really familiar with the Activity lifecycle, it is a really good idea to read up on it in the Android Docs. There is a lot of great information there.

Now, run the app and see how you can rotate the screen and the numbers will persist!

Adding Change Listeners

One thing that our current code limits us in is that we don’t know when the value of the odometer changes. If we were using this as part of a group of widgets that depended on having an up-to-date value from the odometer, it would fail with our current widget. To demonstrate this (before we fix it), add a TextView to the main activity layout (main.xml – I added it after/below the odometer, but you could do it above if you really wanted to):

<TextView
   android:id="@+id/main_valuedisplay"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:textSize="34dp"
   android:textColor="#FFF"
   android:gravity="center"
   />

In addition, change the code in the main activity to show the odometer value in the TextView:

private TextView mValueDisplay;
//...
public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	
	mOdometer = (Odometer) findViewById(R.id.odometer);
	mValueDisplay = (TextView) findViewById(R.id.main_valuedisplay);
	
	if(savedInstanceState != null)
	{
		mOdometerValue = savedInstanceState.getInt(KEY_VALUE);
		mOdometer.setValue(mOdometerValue);
	}
	updateOdometerValue();
}

private void updateOdometerValue()
{
	mOdometerValue = mOdometer.getValue();
	
	String text = String.format("%06d", mOdometerValue);
	mValueDisplay.setText(text);
}

We format the value of the odometer (the “%06d“) to be zero-padded so that it will look more like what the odometer shows. You can also add a call to updateOdometerValue() in onSaveInstanceState() if you wish, but since that code isn’t called until our activity is getting destroyed we won’t see the change anyways. Now run the app, and see that as you change the numbers of the odometer, the text view doesn’t update its display until you rotate the screen. This happens because we only read the odometer value in onSaveInstanceState() – so our activity is only updated when we destroy it!

A view of different values being displayed on the odometer and the value text view
As you change the Odometer, the value displayed below does not change

In order for our activity to keep the TextView updated with the latest odometer value, it needs to know when that value has changed. The value of the odometer changes when any of its digits changes, so we need to know when any of the OdometerSpinners is changed. Add the following code to OdometerSpinner:

public class OdometerSpinner extends View
{
	private OnDigitChangeListener mDigitChangeListener;
	//...
	public void setCurrentDigit(int digit)
	{
		int newVal = digit;
		
		if(newVal < 0)
			newVal = 0;
		if(newVal > 9)
			newVal = 9;
		
		int old = mCurrentDigit;
		mCurrentDigit = newVal;
		
		if(mCurrentDigit != old && mDigitChangeListener != null)
			mDigitChangeListener.onDigitChange(this, mCurrentDigit);
		
		//...
	}
	//...
	public void setOnDigitChangeListener(OnDigitChangeListener listener)
	{
		mDigitChangeListener = listener;
	}
	//...
	public interface OnDigitChangeListener
	{
		abstract void onDigitChange(OdometerSpinner sender, int newDigit);
	}
}

First (last bit of code), we add a public interface defining how our Listener will be called – note that it is inside the OdometerSpinner class! We also add a member variable to store a listener (dealing with multiple/chained listeners is beyond the scope of this tutorial), and a method to set the listener. FInally, within the setCurrentDigit() method, we check that the current digit has changed, and if so call the listener method.

Next, we need to add essentially the same things to Odometer, but also listen for the spinner changes:

public class Odometer extends TableLayout
{
	private OnValueChangeListener mValueChangeListener;
	//...
	private void initialize()
	{
		//...
		
		for(OdometerSpinner s : mDigitSpinners)
		{
			s.setOnDigitChangeListener(new OdometerSpinner.OnDigitChangeListener()
			{
				public void onDigitChange(OdometerSpinner s, int newDigit)
				{
					updateValue();
				}
			});
		}
	}
	
	private void updateValue()
	{
		int value = 0;
		
		for(int i = NUM_DIGITS - 1; i >= 0; --i)
		{
			value = 10 * value + mDigitSpinners[i].getCurrentDigit();
		}
		
		int old = mCurrentValue;
		mCurrentValue = value;
		
		if(old != mCurrentValue && mValueChangeListener != null)
			mValueChangeListener.onValueChange(this, mCurrentValue);
	}
	
	public void setValue(int value)
	{
		for(int i = 0; i < NUM_DIGITS; ++i)
		{
			mDigitSpinners[i].setCurrentDigit(value % 10);
			value /= 10;
		}
	}
		
	public int getValue()
	{	
		return mCurrentValue;
	}
	
	public void setOnValueChangeListener(OnValueChangeListener listener)
	{
		mValueChangeListener = listener;
	}
	//...
	public interface OnValueChangeListener
	{
		abstract void onValueChange(Odometer sender, int newValue);
	}
}

[Commenter Ted Hopp pointed out that we could avoid allocating a new listener for each separate spinner by having Odometer implement OnDigitChangeListener. Another way would be to create a single member for the Listener and pass that one instance to each spinner. I find myself leaning toward that since it doesn’t then expose the DigitChangeListener interface to the outside world.]

Again, we defined a public interface for other classes to use to listen for our value changes, as well as a method to set our listener. We also set the listener for each of the spinners in the odometer, and update our value whenever one does. Note that the code to read the value from the spinners was moved from the getValue() method to the new updateValue() method. Finally, if the value of the odometer is changed (either through updating based on a spinner change or from setValue), we call our listener.

Finally, we need to setup the change listener in our main activity to listen for changes to the odometer value:

public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	
	mOdometer = (Odometer) findViewById(R.id.odometer);
	mValueDisplay = (TextView) findViewById(R.id.main_valuedisplay);
	
	mOdometer.setOnValueChangeListener(new Odometer.OnValueChangeListener()
	{
		@Override
		public void onValueChange(Odometer sender, int newValue)
		{
			updateOdometerValue();
		}
	});
	
	if(savedInstanceState != null)
	{
		mOdometerValue = savedInstanceState.getInt(KEY_VALUE);
		mOdometer.setValue(mOdometerValue);
	}
	updateOdometerValue();
}

We simply call our updateOdometerValue() method, which reads the current odometer value and set the TextView. Now, when you run, you should see that as you change the odometer, the value displayed below also changes.

An image showing the correctly updating value
As you change the value on the Odometer, the value below updates correctly

One final bug we will deal with in this tutorial is best seen in portrait mode. If you drag a number too far without lifting your finger, you will quickly pass the single additional digit we draw either above or below in the spinner. Because of this, you won’t see any more digits in the spinner until you lift your finger and drag again.

A view showing that if we drag a number too far, no more are drawn
If you drag too far, there aren't any more numbers

To fix this, we need to make some changes to OdometerSpinner. Rather than drawing even more numbers (and thus tracking where to draw them, etc), we will simply check during the drag event if the number has been dragged enough to count as a digit change. We will use the height of the spinner as the threshold, since that is how far apart the digits are spaced. When a number is dragged that far, we will change the value of the spinner, which will reset the above and below numbers, allowing for additional dragging. The change we need to make looks like this:

public boolean onTouchEvent(MotionEvent event)
{
	//... ACTION_DOWN ...
	else if(action == MotionEvent.ACTION_MOVE)
	{
		float currentY = event.getY();
		
		float delta = mTouchLastY - currentY;
		mTouchLastY = currentY;
		
		mDigitY -= delta;
		mDigitAboveY -= delta;
		mDigitBelowY -= delta;
		
		// calculate the overall delta (beginning to now)
		float totalDelta = mTouchStartY - currentY;
		
		// If we have scrolled an entire number, change numbers while 
		// keeping the scroll
		if(Math.abs(totalDelta) > mHeight )
		{
			// need to either increase or decrease value
			float postDelta = Math.abs(totalDelta) - mHeight;
			
			if(totalDelta > 0)
			{
				// go DOWN a number
				setCurrentDigit(mDigitBelow);
				mTouchStartY -= mHeight;
				
				mDigitY -= postDelta;
				mDigitBelowY -= postDelta;
				mDigitAboveY -= postDelta;
			}
			else
			{
				// go UP a number
				setCurrentDigit(mDigitAbove);
				mTouchStartY += mHeight;
				
				mDigitY += postDelta;
				mDigitBelowY += postDelta;
				mDigitAboveY += postDelta;
			}
		}
		
		invalidate();
		
		return true;
	}
	//... ACTION_UP ...
}

The first part of the code is the same as before, but now, after we have moved the digits, we check the total drag distance. If we have dragged the equivalent of one digit (the height of the spinner), we can go ahead and change the number appropriately. Depending on the sign of the total delta, we either increment or decrement the number. When we call setCurrentDigit(), it centers each digit. Since we wait until the next digit would be centered anyways to change, that is what we want.

However, it is possible that our code won’t get called exactly when the total scroll delta is equal to the height. In that case, we need to then offset each digit by the difference between the height and how far we actually scrolled so there isn’t any stutter during the drag. Note that we also offset mTouchStartY by the height so that the code in ACTION_UP behaves correctly once a digit has been passed. If we didn’t ‘reset’ the start y-value, whenever the user dragged past a number or two and released, the digit would increment/decrement even if there was a digit perfectly centered.

Now, you can run the app and see that as you continue to scroll, not only do numbers continue to appear, but as you pass digits the value is also continually updated!

That concludes this tutorial – there are a number of ways this widget can be improved, both in appearance and in functionality. You could add a way for the numbers to automatically animate a scroll to a value when set programmatically, or make it look more realistic with better background graphics and shadows.

Source

Grab the source for the completed Part 3 here. (ZIP)

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

11 thoughts on “Android UI Tutorial – Odometer Part III: Building the Odometer”

  1. Great tutorial! I found it very helpful. Thank you for posting it. I have a couple of suggestions…

    The loops in the Odometer class for updating and setting mCurrentValue can be written to run quite a bit faster. In updateValue(), loop from the end of the array:

    int value = 0;
    for (int i = NUM_DIGITS - 1; i >= 0; --i) {
    value = 10 * value + mDigitSpinners[i].getCurrentDigit();
    }

    In setValue():

    for (int i = 0; i < NUM_DIGITS; ++i) {
    mDigitSpinners[i].setCurrentDigit(value % 10);
    value /= 10;
    }

    That avoids the expensive call to Math.pow().

    Also, Odometer could simply implement OnDigitChangeListener and register itself with each digit, avoiding allocating a bunch of separate listener objects.

    This has lots of applications. With a little tweaking, you could even make this into a slot machine simulation. 🙂

    Thanks again.

    1. Thanks for the tips! You are right that that is a much better way to process those changes – I’ll change the code to reflect this.

      You are correct that it is inefficient to create a new listener for each digit when all we really do is call another method. I find myself wanting to avoid implementing OnDigitChangeListener, however, since that would expose that interface to the outside world, and we don’t really want that. But, we can still get performance improvement by just allocating a single listener as a private member and passing it to each digit spinner in turn.

  2. Great tutorial, thank you!
    I’ve been using it as a guide when building my own custom control, and now I’m stuck: Is there an easy way to declare the listener in XML (similar to the android:onClick=”onMyButtonClick” in the stock controls)?

    The reason is that my activity loads one of several layouts, and my control may not be present in all of them. So I’d like to have the control live in XML and a single “onDoSomething(View)” function that’ll maybe not be used…

    1. Excellent question! It is definitely possible, but a bit more involved than I can describe here. So, I did a whole post on it – check it out and let me know what you think!

      Thanks!

  3. Hi Kevin, this is a great tutorial! It covers a wide range of issues in the pursuit of creating a custom UI widget. As a coder I am new to Java/Android, so it was a pleasure to drink in this new knowledge. Thanks so much for creating it!

  4. hey,
    thanks for the tutorial, its been great to work through and see how things can develop.

    Just wondering if on ‘MotionEvent.ACTION_UP’ it would be too hard to animate scroll to the current number rather than just switch to the current number?

    Thanks for any advice,
    Marc

  5. Thank you for this good guide! It helped me in learning about creating custom widget in Android.
    I’m sorry for my english but I needed to thank you!
    Regards from Italy.
    Flavio Capaccio

  6. Greetings from Colorado! I’m bored to death at work so I decided to check out your website on my iphone during lunch break. I really like the knowledge you present here and can’t wait to take a look when I get home.
    I’m amazed at how quick your blog loaded on my phone .. I’m not even using WIFI, just 3G .
    . Anyhow, excellent blog!

Comments are closed.