In Chapter 11, you saw how fields could have constraints placed on them to limit possible input, such as numeric-only or phone-number-only. These sorts of constraints help users “get it right” when entering information, particularly on mobile devices with cramped keyboards.
Of course, the ultimate in constrained input is to allow selection only from a set of items, such as a group of radio buttons. Classic UI toolkits have list boxes, combo boxes, drop-down lists, and the like for that very purpose. Android provides many of the same sorts of widgets, plus others of particular interest for mobile devices (e.g., the Gallery
for examining saved photos).
Moreover, Android offers a flexible framework for determining which choices are available in these widgets. Specifically, Android offers a framework of data adapters that provides a common interface for selection lists, ranging from static arrays to database contents. Selection views—widgets for presenting lists of choices—are handed an adapter to supply the actual choices.
In the abstract, adapters provide a common interface to multiple disparate APIs. More specifically, in Android’s case, adapters provide a common interface to the data model behind a selection-style widget, such as a list box. This use of Java interfaces is fairly common (e.g., Java/Swing’s model adapters for JTable
), and Java is far from the only environment offering this sort of abstraction (e.g., Flex’s XML data-binding framework accepts XML inlined as static data or retrieved from the Internet).
Android’s adapters are responsible not only for providing the roster of data for a selection widget, but also for converting individual elements of data into specific views to be displayed inside the selection widget. The latter facet of the adapter system may sound a little odd, but in reality, it is not that different from other GUI toolkits’ ways of overriding default display behavior. For example, in Java/Swing, if you want a JList
-backed list box to actually be a checklist (where individual rows are a check box plus label, and clicks adjust the state of the check box), you inevitably wind up calling setCellRenderer()
to supply your own ListCellRenderer
, which in turn converts strings for the list into JCheckBox
-plus-JLabel
composite widgets.
The easiest adapter to use is ArrayAdapter
. You simply wrap one of these around a Java array or java.util.List
instance, and you have a fully functioning adapter:
String[] items={"this", "is", "a",
"really", "silly", "list"};
new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, items);
One flavor of the ArrayAdapter
constructor takes three parameters:
Context
to use (typically this will be your activity instance)By default, the ArrayAdapter
will invoke toString()
on the objects in the list and wrap each of those strings in the view designated by the supplied resource. android.R.layout.simple_list_item_1
simply turns those strings into TextView
objects. Those TextView
widgets, in turn, will be shown in the list, spinner, or whatever widget uses this ArrayAdapter
. If you want to see what android.R.layout.simple_list_item_1
looks like, you can find a copy of it in your SDK installation—just search for simple_list_item_1.xml
.
In Chapter 13, you’ll see how to subclass an adapter and override row creation, to give you greater control over how rows appear.
The classic list box widget in Android is known as ListView
. Include one of these in your layout, invoke setAdapter()
to supply your data and child views, and attach a listener via setOnItemSelectedListener()
to find out when the selection has changed. With that, you have a fully functioning list box.
However, if your activity is dominated by a single list, you might consider creating your activity as a subclass of ListActivity
, rather than the regular Activity
base class. If your main view is just the list, you do not even need to supply a layout—ListActivity
will construct a full-screen list for you. If you do want to customize the layout, you can, as long as you identify your ListView
as @android:id/list
, so ListActivity
knows which widget is the main list for the activity.
For example, here is a layout pulled from the Selection/List
sample project, a simple list with a label on top to show the current selection:
<?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:id="@+id/selection"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:drawSelectorOnTop="false"
/>
</LinearLayout>
The Java code to configure the list and connect the list with the label is as follows:
public class ListViewDemo extends ListActivity {
private TextView selection;
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
setListAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
items));
selection=(TextView)findViewById(R.id.selection);
}
public void onListItemClick(ListView parent, View v, int position,
long id) {
selection.setText(items[position]);
}
}
With ListActivity
, you can set the list adapter via setListAdapter()
—in this case, providing an ArrayAdapter
wrapping an array of nonsense strings. To find out when the list selection changes, override onListItemClick()
and take appropriate steps based on the supplied child view and position—in this case, updating the label with the text for that position. The results are shown in Figure 12–1.
The second parameter to our ArrayAdapter
, android.R.layout.simple_list_item_1
, controls the appearance of the rows. The value used in the preceding example provides the standard Android list row: big font, a lot of padding, and white text.
By default, ListView
is set up to simply collect clicks on list entries. If you want a list that tracks a user’s selection, or possibly multiple selections, ListView
can handle that as well, but it requires a few changes.
First, you need to call setChoiceMode()
on the ListView
in Java code to set the choice mode, supplying either CHOICE_MODE_SINGLE
or CHOICE_MODE_MULTIPLE
as the value. You can get your ListView
from a ListActivity
via getListView()
. You can also declare this via the android:choiceMode
attribute in your layout XML.
Then, instead of using android.R.layout.simple_list_item_1
as the layout for the list rows in your ArrayAdapter
constructor, you need to use either android.R.layout.simple_list_item_single_choice
or android.R.layout.simple_list_item_multiple_choice
for single-choice or multiple-choice lists, respectively.
For example, here is an activity layout from the Selection/Checklist
sample project:
<?xml version="1.0" encoding="utf-8"?>
<ListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:drawSelectorOnTop="false"
android:choiceMode="multipleChoice"
/>
It is a full-screen ListView
, with the android:choiceMode="multipleChoice"
attribute to indicate that we want multiple-choice support.
Our activity simply uses a standard ArrayAdapter
on our list of nonsense words, but uses android.R.layout.simple_list_item_multiple_choice
as the row layout:
package com.commonsware.android.checklist;
import android.os.Bundle;
import android.app.ListActivity;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class ChecklistDemo extends ListActivity {
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
setListAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_multiple_choice,
items));
}
}
The user sees the list of words on the left with check boxes down the right edge, as shown in Figure 12–2.
If we wanted to, we could call getCheckedItemPositions()
on our ListView
to find out which items the user checked, or setItemChecked()
to check (or uncheck) a specific entry ourselves.
In Android, the Spinner
is the equivalent of the drop-down selector you might find in other toolkits (e.g., JComboBox
in Java/Swing). Pressing the center button on the D-pad pops up a selection dialog box from which the user can choose an item. The Spinner
basically provides list selection capabilities without taking up all the screen space of a ListView
, at the cost of an extra click or screen tap to make a change.
As with ListView
, you provide the adapter for data and child views via setAdapter()
, and hook in a listener object for selections via setOnItemSelectedListener()
.
If you want to tailor the view used when displaying the drop-down perspective, you need to configure the adapter, not the Spinner
widget. Use the setDropDownViewResource()
method to supply the resource ID of the view to use.
For example, culled from the Selection/Spinner
sample project, here is an XML layout for a simple view with a Spinner
:
<?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:id="@+id/selection"
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"
/>
</LinearLayout>
This is the same view as shown in the previous section, but with a Spinner
instead of a ListView
. The Spinner
property android:drawSelectorOnTop
controls whether the arrow is drawn on the selector button on the right side of the Spinner
UI.
To populate and use the Spinner
, we need some Java code:
public class SpinnerDemo extends Activity
implements AdapterView.OnItemSelectedListener {
private TextView selection;
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
selection=(TextView)findViewById(R.id.selection);
Spinner spin=(Spinner)findViewById(R.id.spinner);
spin.setOnItemSelectedListener(this);
ArrayAdapter<String> aa=new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item,
items);
aa.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
spin.setAdapter(aa);
}
public void onItemSelected(AdapterView<?> parent,
View v, int position, long id) {
selection.setText(items[position]);
}
public void onNothingSelected(AdapterView<?> parent) {
selection.setText("");
}
}
Here, we attach the activity itself as the selection listener (spin.setOnItemSelectedListener(this)
). This works because the activity implements the OnItemSelectedListener
interface. We configure the adapter not only with the list of fake words, but also with a specific resource to use for the drop-down view (via aa.setDropDownViewResource()
). Also note the use of android.R.layout.simple_spinner_item
as the built-in View
for showing items in the spinner itself.
Finally, we implement the callbacks required by OnItemSelectedListener
to adjust the selection label based on user input. Figures 12–3 and 12–4 show the results.
As the name suggests, GridView
gives you a two-dimensional grid of items to choose from. You have moderate control over the number and size of the columns; the number of rows is dynamically determined based on the number of items the supplied adapter says are available for viewing.
There are a few properties that, when combined, determine the number of columns and their sizes:
android:numColumns
: Indicates how many columns there are, or, if you supply a value of auto_fit
, Android will compute the number of columns based on the available space and the following properties in this list.android:verticalSpacing
and android:horizontalSpacing
: Indicate how much whitespace should exist between items in the grid.android:columnWidth
: Indicates how many pixels wide each column should be.android:stretchMode
: Indicates, for grids with auto_fit
for android:numColumns
, what should happen for any available space not taken up by columns or spacing. This can be columnWidth
, to have the columns take up available space, or spacingWidth
, to have the whitespace between columns absorb extra space.Otherwise, the GridView
works much like any other selection widget—use setAdapter()
to provide the data and child views, invoke setOnItemSelectedListener()
to register a selection listener, and so on.
For example, here is an XML layout from the Selection/Grid
sample project, showing a GridView
configuration:
<?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:id="@+id/selection"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<GridView
android:id="@+id/grid"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:verticalSpacing="40dip"
android:horizontalSpacing="5dip"
android:numColumns="auto_fit"
android:columnWidth="100dip"
android:stretchMode="columnWidth"
android:gravity="center"
/>
</LinearLayout>
For this grid, we take up the entire screen except for what our selection label requires. The number of columns is computed by Android (android:numColumns = "auto_fit"
) based on our horizontal spacing (android:horizontalSpacing = "5dip"
) and column width (android:columnWidth = "100dip"
), with the columns absorbing any “slop” width left over (android:stretchMode = "columnWidth"
).
The Java code to configure the GridView
is as follows:
package com.commonsware.android.grid;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import android.widget.TextView;
public class GridDemo extends Activity
implements AdapterView.OnItemSelectedListener {
private TextView selection;
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
selection=(TextView)findViewById(R.id.selection);
GridView g=(GridView) findViewById(R.id.grid);
g.setAdapter(new ArrayAdapter<String>(this,
R.layout.cell,
items));
g.setOnItemSelectedListener(this);
}
public void onItemSelected(AdapterView<?> parent, View v,
int position, long id) {
selection.setText(items[position]);
}
public void onNothingSelected(AdapterView<?> parent) {
selection.setText("");
}
}
The grid cells are defined by a separate res/layout/cell.xml
file, referenced in our ArrayAdapter
as R.layout.cell
:
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14dip"
/>
With the vertical spacing from the XML layout (android:verticalSpacing = "40dip"
), the grid overflows the boundaries of the emulator’s screen, as shown in Figures 12–5 and 12–6.
The AutoCompleteTextView
is sort of a hybrid between the EditText
(field) and the Spinner
. With autocompletion, as the user types, the text is treated as a prefix filter, comparing the entered text as a prefix against a list of candidates. Matches are shown in a selection list that drops down from the field (as with Spinner
). The user can either type the full entry (e.g., something not in the list) or choose an item from the list to be the value of the field.
AutoCompleteTextView
subclasses EditText
, so you can configure all the standard look-and-feel aspects, such as font face and color. In addition, AutoCompleteTextView
has an android:completionThreshold
property, to indicate the minimum number of characters a user must enter before the list filtering begins.
You can give AutoCompleteTextView
an adapter containing the list of candidate values via setAdapter()
. However, since the user could type something that is not in the list, AutoCompleteTextView
does not support selection listeners. Instead, you can register a TextWatcher
, as you can with any EditText
widget, to be notified when the text changes. These events will occur either because of manual typing or from a selection from the drop-down list.
The following is a familiar XML layout, this time containing an AutoCompleteTextView
(pulled from the Selection/AutoComplete
sample application):
<?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:id="@+id/selection"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<AutoCompleteTextView android:id="@+id/edit"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:completionThreshold="3"/>
</LinearLayout>
The corresponding Java code is as follows:
package com.commonsware.android.auto;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;
public class AutoCompleteDemo extends Activity
implements TextWatcher {
private TextView selection;
private AutoCompleteTextView edit;
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue", "purus"};
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
selection=(TextView)findViewById(R.id.selection);
edit=(AutoCompleteTextView)findViewById(R.id.edit);
edit.addTextChangedListener(this);
edit.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_dropdown_item_1line,
items));
}
public void onTextChanged(CharSequence s, int start, int before,
int count) {
selection.setText(edit.getText());
}
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
// needed for interface, but not used
}
public void afterTextChanged(Editable s) {
// needed for interface, but not used
}
}
This time, our activity implements TextWatcher
, which means our callbacks are onTextChanged()
, beforeTextChanged()
, and afterTextChanged()
. In this case, we are interested only in onTextChanged()
, and we update the selection label to match the AutoCompleteTextView
’s current contents. Figures 12–7, 12–8, and 12–9 show the results.
The Gallery
widget is not one ordinarily found in GUI toolkits. It is, in effect, a list box that is laid out horizontally. One choice follows the next across the horizontal plane, with the currently selected item highlighted. On an Android device, the user rotates through the options via the left and right D-pad buttons.
Compared to the ListView
, the Gallery
takes up less screen space, while still showing multiple choices at one time (assuming they are short enough). Compared to the Spinner
, the Gallery
always shows more than one choice at a time.
The quintessential example use for the Gallery
is image preview. Given a collection of photos or icons, the Gallery
lets people preview the pictures in the process of choosing one.
Code-wise, the Gallery
works much like a Spinner
or GridView
. In your XML layout, you have a few properties at your disposal:
android:spacing
: Controls the number of pixels between entries in the list.android:spinnerSelector
: Controls what is used to indicate a selection. This can either be a reference to a Drawable
(see the resources chapter) or an RGB value in #AARRGGBB
or similar notation.android:drawSelectorOnTop
: Indicates if the selection bar (or Drawable
) should be drawn before (false
) or after (true
) drawing the selected child. If you choose true
, be sure that your selector has sufficient transparency to show the child through the selector; otherwise, users will not be able to read the selection.