Smartphones

You spin me right round: Creating a custom 'spinner' in Android

Developer William Francis used the Android Animation Framework to implement a custom "spinner" in a list view. He describes how to do it, and warns you about a couple of gotchas in the process.

Any experienced engineer will tell you that each platform has its strengths and its weaknesses. Being the lead Android developer on a team who also writes software for iPhone and Windows Phone 7, I am frequently reminded how "loose" Google's UI framework is. That's not to say I don't love the Android's open architecture -- I do. Android is a powerful operating system, and there is very little it can't do. But the effort it takes to perform some of the slick transition effects and maintain the consistent presentation that simply "is" on the Apple and Microsoft mobile platforms can be a bit daunting to developers getting their feet wet in Google's digital pool.

Recently I had to implement my own custom "spinner" in a list view. You know what I mean -- one of those circular images that spins right round (baby right round) to indicate something is loading. While there is a ProgressBar class in the Android framework, this introduces the complexity of an additional view and that seemed like unnecessary overhead for such a trivial task. So I boned up a bit on the current state of the Animation framework, threw up an image view, and gave it a try.

While I was pleased to discover the Google UI framework team is making strides in the right direction, I ran into a couple bumps along the way with classes not behaving as I would have expected, and in one instance a class didn't behave the way the folks at Google expected either (or at the very least, the engineer who wrote the documentation for the class didn't have a solid understanding of the function). After a few days of coding, several re-reads of the documentation, and a couple posts in the Google code forums, I eventually had my "spinner" up and spinning. The tutorial that follows will show you how to do the same, while hopefully saving you from discovering the nefarious gotchas the hard way.

The entire source code for the demo can be downloaded as a compressed archive here. I've included three images in the resource folder, which I found on Google Images. (Trust me... no one wants me drawing images from scratch. I'm still haunted by the memory of the time in grade school art class we had to sketch our pet and, despite my best effort at a dog, I was sent to the principal's office for what my teacher thought was surely a lewd prank.) I didn't see any copyright information on the images I selected, but if you are the creator / owner of one of the images I used and would like me to use something else or give you some credit in the source files just drop me a line.

<!--[if !supportLists]-->1. <!--[endif]-->Create a new Android project in Eclipse using the plug-in. I always try to support the widest range of phones possible and, as such, you may build this project for any version of the OS greater than or equal to Android 1.6 (Cupcake).

<!--[if !supportLists]-->2. <!--[endif]-->Add whatever drawables you plan to use to the appropriate resource folder. In my case, I have three images titled: flower_icon.png, frog_icon.png, and gear_icon.png.

<!--[if !supportLists]-->3. <!--[endif]-->Create an /anim folder in the /res directory. This is where you will add the XML files that define your "tween" animations. The Google tween animation engine is impressive. Besides rotations, it is capable of performing scale operations, alpha channel animations, and translate manipulations.

<!--[if !supportLists]-->4. <!--[endif]-->Add the following XML animation definitions:

<!--[if !supportLineBreakNewLine]-->

<!--[endif]-->

alpha.xml

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

<alpha

xmlns:android="http://schemas.android.com/apk/res/android"

android:fromAlpha="1.0"

android:toAlpha="0.0"

android:duration="2000" />

rotate.xml <?xml version="1.0" encoding="UTF-8"?>

<rotate

xmlns:android="http://schemas.android.com/apk/res/android"

android:fromDegrees="0"

android:toDegrees="360"

android:pivotX="50%"

android:pivotY="50%"

android:interpolator="@android:anim/linear_interpolator"

android:duration="2000" />

scale.xml

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

<scale

xmlns:android="http://schemas.android.com/apk/res/android"

android:fromXScale="1"

android:toXScale=".25"

android:fromYScale="1"

android:toYScale=".25"

android:pivotX="50%"

android:pivotY="50%"

android:duration="2000" />

Gotcha: The documentation says you can set the repeat count property in the XML file, but in my experience, it doesn't always work this way, particularly when you use animation sets to create combined animation effects. I recommend you get in the habit of setting the repeat count property in your Java code, and I will show you how to do so in my custom list view adapter class.

<!--[if !supportLists]-->5. <!--[endif]-->In your /layout folder, add a standard list view to your main layout.

main.xml

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

<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"/>

<ListView

android:id="@+id/android:list"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:cacheColorHint="#0000"

android:background="@android:color/transparent"

android:dividerHeight="1dip"/>

</LinearLayout>

<!--[if !supportLists]-->6. <!--[endif]-->Define the list view row. I used a table layout with one text and one image view.

list_view_row.xml

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="?android:attr/listPreferredItemHeight"

android:padding="6dip">

<TableLayout android:orientation="horizontal"

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:layout_marginBottom="2dip"

android:stretchColumns="*">

<TableRow>

<TextView

android:id="@+id/label"

android:textSize="18sp"

android:padding="2dip"

android:gravity="left|center"

android:layout_gravity="left|center"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textColor="#ffffff"/>

<ImageView

android:id="@+id/gfx"

android:padding="2dip"

android:gravity="right|center"

android:layout_gravity="right|center"

android:layout_width="fill_parent"

android:layout_height="wrap_content"/>

</TableRow>

</TableLayout>

</LinearLayout>

<!--[if !supportLists]-->7. <!--[endif]-->Move on to the source code. Using the standard listview / adapter hierarchy, the items in your list must keep up with their own state for redrawing and, as such, we require a simple container of our own making.

MyContainer.java

package authorwjf.com;

public class MyContainer {

private String mText = "";

private Boolean mAnimating = false;

private int mDrawable = -1;

public void setText(String someText) { mText = someText; }

public String getText() { return mText; }

public void setAnimating(Boolean isAnimating) { mAnimating = isAnimating; }

public Boolean isAnimating() { return mAnimating;}

public void setDrawableResourceId(int id) { mDrawable = id; };

public int getDrawableResourceId() { return mDrawable; }

}

<!--[if !supportLists]-->8. <!--[endif]-->The main class does little more than load our list view and toggle the drawing flag when an item in the list is selected.

Main.java package authorwjf.com;

import java.util.ArrayList;

import android.app.ListActivity;

import android.os.Bundle;

import android.view.View;

import android.widget.ListView;

public class Main extends ListActivity {

private ArrayList<MyContainer> mItems = null;

private CustomAdapter mAdapter;

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mItems = new ArrayList<MyContainer>();

MyContainer item1 = new MyContainer();

item1.setAnimating(false);

item1.setText("Rotatation Tween Effect");

item1.setDrawableResourceId(R.drawable.gear_icon);

mItems.add(item1);

MyContainer item2 = new MyContainer();

item2.setAnimating(false);

item2.setText("Alpha Tween Effect");

item2.setDrawableResourceId(R.drawable.frog_icon);

mItems.add(item2);

MyContainer item3 = new MyContainer();

item3.setAnimating(false);

item3.setText("Scale Tween Effect");

item3.setDrawableResourceId(R.drawable.flower_icon);

mItems.add(item3);

mAdapter = new CustomAdapter(this, R.layout.list_view_row, mItems);

setListAdapter(mAdapter);

}

/**

* Listview on click handler.

*/

@Override

public void onListItemClick(ListView parent, View v, int position, long id){

mItems.get(position).setAnimating(!mItems.get(position).isAnimating());

mAdapter.notifyDataSetChanged();

}

}

<!--[if !supportLists]-->9. <!--[endif]-->Last, but certainly not least is our custom adapter responsible for turning on and off the animations. This is where I'm setting the animation repeat property (anim.setRepeatCount(Animation.INFINITE)) rather than in the XML files as the Google documentation often suggests.

CustomAdapter.java

package authorwjf.com;

import java.util.ArrayList;

import android.content.Context;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.view.animation.Animation;

import android.view.animation.AnimationUtils;

import android.widget.ArrayAdapter;

import android.widget.ImageView;

import android.widget.TextView;

public class CustomAdapter extends ArrayAdapter<MyContainer> {

private ArrayList<MyContainer> items;

private Context c = null;

public CustomAdapter(Context context, int textViewResourceId, ArrayList<MyContainer> items) {

super(context, textViewResourceId, items);

this.items = items;

this.c = context;

}

@Override

public View getView(int position, View convertView, ViewGroup parent) {

View v = convertView;

if (v == null) {

LayoutInflater vi = (LayoutInflater)c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

v = vi.inflate(R.layout.list_view_row, null);

}

TextView label = (TextView) v.findViewById(R.id.label);

ImageView gfx = (ImageView) v.findViewById(R.id.gfx);

MyContainer item = (MyContainer) items.get(position);

Animation anim = null;

switch (item.getDrawableResourceId()) {

case R.drawable.flower_icon:

anim = AnimationUtils.loadAnimation(getContext(), R.anim.scale);

break;

case R.drawable.frog_icon:

anim = AnimationUtils.loadAnimation(getContext(), R.anim.alpha);

break;

case R.drawable.gear_icon:

anim = AnimationUtils.loadAnimation(getContext(), R.anim.rotate);

break;

}

label.setText(item.getText());

gfx.setImageResource(item.getDrawableResourceId());

gfx.clearAnimation();

gfx.setVisibility(View.VISIBLE);

if (item.isAnimating()) {

anim.setRepeatCount(Animation.INFINITE);

anim.setRepeatMode(Animation.REVERSE);

gfx.startAnimation(anim);

}

return v;

}

}

Gotcha: For reasons I fail to fully comprehend, you must call .clearAnimation every time your adapter is invoked. The behavior you get otherwise is wonky and suggests the UI framework re-uses views in a less than ideal manner. If you'd like to see what I'm talking about, comment out the line gfx.clearAnimation(); and then load the application to your device and tap on a couple different rows. You will see rows affected that you did not tap on, and in a manner that is not in any apparent way logical. I broached this subject with a Google UI framework engineer and never got a satisfactory answer.

There you have it, a little UI love for Google's favorite robot. To my fellow Android developers I say: "Behold! Apple's iPhone isn't the only device out there that can have a slick UI. It's just at times a little more work to get there from here."

Screen shot of our animations running on the emulator.

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

1 comments
gradkiss
gradkiss

It's good to see so much care and input from others in something so valuable to our grandchildren's grandchildren. I welcome all contributions to open source...free software, etc. Thanks for your time and efforts.

Editor's Picks