Android: Spinners, SimpleAdapter, and (maybe) ViewBinder

The question came up on StackOverflow yesterday: Can a Spinner be configured to use a SimpleAdapter (and if so, how?) The user who asked the question, Chromium, ran into a couple of problems; the last of these was an IllegalStateException after clicking on the Spinner to make a selection.

A bit of searching turned up this issue about using a Spinner in combination with a SimpleAdapter to display a CheckedTextView; it seems that a ViewBinder must be set on the SimpleAdapter for that combination to work. Although it’s not the same problem, it still made me wonder if the SimpleAdapter / Spinner combination might need a ViewBinder for displaying TextView, too.

I wasn’t able to reproduce the IllegalStateException when I ran a sample program on AndroidOS 2.2, but I thought I’d try it on 1.5 in the emulator. Sure enough, I hit the same problem that Chromium reported. Apparently, whatever issue this is has been fixed by 2.2, and perhaps in earlier releases. (I haven’t tried other releases besides 2.2 and 1.5.)

And I also found that ViewBinder once again comes to the rescue, preventing the IllegalStateException from being thrown.

Like some other interfaces in Android (I’m looking at you, CursorToStringConverter. You too, FilterQueryProvider), ViewBinder’s bark is worse than its bite. In other words, the name may be a bit intimidating, but the implementation is a breeze.

Well, it would be a breeze, perhaps, if the documentation were clearer. The only method that you need to implement, setViewValue, receives three parameters: View view, Object data, String textRepresentation. But the documentation doesn’t tell us what these parameters really mean in a given context. I started by creating an empty setViewValue method, and set a breakpoint there. Inside the breakpoint, I verified that view is the TextView for displaying a single row in the Spinner; data is the String value in the Map for this row in the Spinner (“Red”, “Orange”, etc.); and textRepresentation is, well, the text representation of data, which in this case is that same String.

Here’s a sample program, adapted from Chromium’s example, that adds a ViewBinder to a SimpleAdapter. This works on AndroidOS 1.5 and 2.2 (and, presumably, intermediate versions as well.)

package org.oowb.HelloSpinner;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.SimpleAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemSelectedListener;

/**
 *  Displays a list of colors in a Spinner.
 */
public class HelloSpinner extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // The key to use for reading the color from the Map
        final String[] from = new String[] { "color" };

        // The type of View to use for displaying the color name.
        // android.R.id.text1 is a standard resource for displaying text.
        final int[] to = new int[] { android.R.id.text1  };

        // Create the List of strings for the spinner to display. Each string
        // is embedded within a Map, using "color" as the key.
        final List<Map<String, String>> data = 
            new ArrayList<Map<String, String>>();
        final String[] colors = getResources().getStringArray(R.array.colors);

        for (int i = 0; i < colors.length; i++) {
            data.add(addData(colors[i]));
        }

        final SimpleAdapter simpleAdapter = 
            new SimpleAdapter(this, data, 
                    android.R.layout.simple_spinner_item, from, to);
        simpleAdapter.setDropDownViewResource(
                android.R.layout.simple_spinner_dropdown_item);

        // Add a ViewBinder to display a color name in a TextView within the
        // Spinner. (This isn't needed in AndroidOS 2.2. In earlier releases,
        // when we're displaying text data within a Spinner, and no ViewBinder
        // is set in the SimpleAdapter, an IllegalStateException is thrown.)
        SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
            
            public boolean setViewValue(View view, Object data,
                    String textRepresentation) {
                // We configured the SimpleAdapter to create TextViews (see
                // the 'to' array, above), so this cast should be safe:
                TextView textView = (TextView) view;
                textView.setText(textRepresentation);
                return true;
            }
        };
        simpleAdapter.setViewBinder(viewBinder);

        final Spinner spinner = (Spinner) findViewById(R.id.spinner);
        spinner.setAdapter(simpleAdapter);

        // Add an OnItemSelectedListener to display the selected Color
        spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view,
                    int position, long id) {
                // Get the color name out of the Map
                final Map<String, String> data = 
                    (Map<String, String>) parent.getItemAtPosition(position);
                final String text = "Selected Color:-  " + data.get("color");

                Toast.makeText(parent.getContext(), text, 
                        Toast.LENGTH_LONG).show();
            }

            @Override
            public void onNothingSelected(AdapterView<?> arg0) {
                // Do nothing
            }
        });
    }

    /**
     * Convert the String that's passed in into a Map, with
     * "color" as the key, and the String as the value.
     * @param    colorName  The color to be inserted into a new Map
     * @return   the new Map
     */
    private Map<String, String> addData(String colorName) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("color", colorName);
        return map;
    }
}

To complete the example, here are the layout (main.xml) and values files (arrays.xml and strings.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:padding="10dip"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">

    <Spinner 
        android:id="@+id/spinner"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:drawSelectorOnTop="true"
        android:prompt="@string/prompt"
    />
</LinearLayout>
<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <string-array name="colors">
        <item>Red</item>
        <item>Orange</item>
        <item>Yellow</item>
        <item>Green</item>
        <item>Blue</item>
        <item>Purple</item>
        <item>Violet</item>
        <item>Octarine</item>
    </string-array>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Color Chooser</string>
    <string name="prompt">Choose a color:</string>
</resources>
, , ,

4 Comments

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>