Chapter 8. Other GUI Elements: Lists and Views

It may seem odd to have a separate chapter for the RecyclerView and ListView components. But these are, in fact, among the most important GUI components, being used in probably 80% of all Android applications. And these list components are very flexible; you can do a lot with them, but figuring out how to do it is sometimes not as intuitive as it could be.

In this chapter we cover topics from basic RecyclerView and ListView uses through to advanced uses.

So why are there two list components? ListView has been around since the beginning of Android time. RecyclerView was introduced around 2015 as a more modern replacement, but many applications still use the original ListView, so we discuss both.

A good overview of ListView can be found in a Google I/O 2010 talk that’s available on Google’s YouTube channel; this was presented by Google employees Romain Guy and Adam Powell, who work on the code for ListView.

8.1 Building List-Based Applications with RecyclerView

Ian Darwin

Problem

RecyclerView is a modern reinterpretation of the classical ListView. You want to learn when and how to use the new paradigm.

Solution

Use a RecyclerView.

Discussion

You could argue that RecyclerView is badly named. It should have been called ListView2 or something similar, to tie it in to the ListView, which it aims to replace. Paraphrasing Dr. Seuss: “But they didn’t, and now it’s too late.” It’s called RecyclerView because it is better at recycling View objects than its predecessor. Thus, it is more efficient, especially for dealing with large lists.

To build a RecyclerView application, you need to do the following:

  • Provide an Activity with a RecyclerView as part of its view layout.

  • Provide a RecyclerView.Adapter implementation with several methods.

  • Provide a ViewHolder class as part of your adapter implementation.

Our simple example has the following layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.androidcookbook.recyclerviewdemo.ListActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

The containing RelativeLayout is not needed, but you will usually have at least one other control in a real example.

Our simple Activity class using this layout contains the obvious imports and fields, and the following in its onCreate() method:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_list);

    mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    mAdapter =
        new MyListAdapter(getResources().getStringArray(R.array.foodstuffs));
    mRecyclerView.setAdapter(mAdapter);

    mLayoutManager = new LinearLayoutManager(this);
    mRecyclerView.setLayoutManager(mLayoutManager);
}

The Adapter class, like any other adapter, is responsible for converting data between its internal form in the application and the View components used to display it. The inheritance is different from the normal Adapter class, however. The RecyclerView.Adapter must implement the following:

public class MyListAdapter
        extends RecyclerView.Adapter<MyListAdapter.ViewHolder> {
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);
    public void onBindViewHolder(ViewHolder holder, int position);
    public int getItemCount();
}

The first method is called to actually create a new ViewHolder, which will usually encapsulate the actual View class to be used for one “row” in the list—it may be a ViewGroup of course, since a ViewGroup is a View.

The second method is where the “recycling” happens—in this method, we must populate a ViewHolder. Obviously it will be one that we previously created in onCreateViewHolder(), but we make no assumptions about whether it’s previously been populated. All we know is that it’s our time to populate it with the data at the supplied position within the list of data.

Of course, getItemCount() returns the size of our list. Our example is simple, just a list of String values which are statically allocated, so this method is trivial to implement.

Both ListView and RecyclerView need a separate XML layout to specify the View object(s) comprising the individual rows. Our example, like most introductory list recipes, contains just a TextView; the XML for this appears in Example 8-1.

Example 8-1. row_item.xml (complete)
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/textview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

Here is the actual code for these methods in our RecyclerView.Adapter implementation:

public class MyListAdapter
        extends RecyclerView.Adapter<MyListAdapter.ViewHolder> {

    private static final String TAG = "CustomAdapter";
    String[] mData;

    public MyListAdapter(String[] data) {
        mData = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        final Context context = viewGroup.getContext();
        return new ViewHolder(context, LayoutInflater.from(context)
                .inflate(R.layout.row_item, viewGroup, false));
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Log.d(TAG, "onBindViewHolder(" + position + ")");
        TextView v = holder.getView();
        v.setText(mData[position]);
    }

    @Override
    public int getItemCount() {
        return mData.length;
    }
    ...
}

Finally, here is the code for our ViewHolder implementation, whose job is to hold onto a View on behalf of the RecyclerView. We pass the Context into our ViewHolder constructor only for use in generating a toast to show when the user taps an item (this is arguably not best practice, just an expedient to make the example shorter):

class ViewHolder extends RecyclerView.ViewHolder {

    private TextView mTextView; // The View we hold

    public ViewHolder(final Context context, View itemView) {
        super(itemView);
        itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "You clicked " + mData[getPosition()],
                Toast.LENGTH_SHORT).show();
            }
        });
        mTextView = (TextView) itemView.findViewById(R.id.textview);
    }

    public TextView getView() {
        return mTextView;
    }
}

See Also

The developer documentation on RecyclerView.

8.2 Building List-Based Applications with ListView

Jim Blackler

Problem

Many mobile applications follow a similar pattern, allowing users to browse and interact with multiple items in a list. How can you use standard Android UI classes to quickly build an app that works the way users will expect, providing them a list-based view of their data?

Solution

Use a ListView, an extremely versatile control that is well suited to the screen size and control constraints of a mobile application, displaying information in a vertical stack of rows. This recipe shows how to set up a ListView, including rows that contain any combination of standard UI views.

Discussion

Many Android applications are based on the ListView control. It solves the problem of how to present a lot of information in a way that’s easy for the user to browse, displaying information in a vertical stack of rows that the user can scroll through. As the user approaches the results at the end of the list, more results can be generated and added. This allows result paging in a natural and intuitive manner.

Android’s ListView helps organize your code by separating browsing and editing operations into separate Activities. A ListView simply requires the user to press somewhere in the row, which works well on a small, finger-operated screen. When the row is clicked, a new Activity can be launched that can contain further options to manipulate the data shown in the row.

Another advantage of the ListView format is that it allows paging in an uncomplicated way. Paging is where all the information requested by a user cannot feasibly be shown at once. For instance, the user may be browsing his email inbox, which contains 2,000 messages; it would not be feasible to download all 2,000 from the email server, and nor would it be required, as the user will probably only scan the first 10 or so messages.

Most web applications handle this problem by segmenting the results into pages, and having controls in the footer that allow the user to navigate through them. With a ListView, the application can retrieve an initial batch of results, which are shown to the user in a list. When the user reaches the end of the list, a final row is seen, containing an indeterminate progress bar. As this comes into view, the application can fetch the next batch of results in the background. When they are ready to be shown, the last progress bar row is replaced with rows containing the new data. The user’s view of the list is not interrupted, and new data is fetched purely on demand.

To implement a ListView in your Android application, you need an Activity layout to host it. This should contain a ListView control configured to take up most of the screen layout. This allows other elements such as progress bars and extra overlaid indicators to be included in the layout.

While many Android experts (including most of the other contributors to this chapter) recommend using the ListActivity, I personally do not recommend this. It supplies little extra logic over a plain Activity, but using it restricts the form of the inheritance tree your application’s Activities can take. For instance, it is very common that all Activities will inherit from a single common Activity, such as ApplicationActivity, supplying common functionality such as an About box or Help menu. This pattern isn’t possible if some Activities are inherited from ListActivity and some are directly inherited from Activity. That said, you will see most of the examples in this book doing it the “official” way. As with all such things, choose one way or the other, and try to stick with it.

An application controls the data added to a ListView by supplying a ListAdapter using the setListAdapter() method. There are 13 functions that a ListAdapter is expected to supply. However, if a BaseAdapter is used, this reduces the number of functions supplied to four, representing the minimum functionality that must be supplied. The adapter specifies the number of item rows in the list, and is expected to supply a View object to represent any item given its row number. It is also expected to return both an object and an object ID to represent any given row number. This is to aid advanced list features such as row selection (not covered in this recipe).

I suggest starting with the most versatile type of ListAdapter, the BaseAdapter (android.widget.BaseAdapter). This allows any layout to be specified for a row (multiple layouts can be matched to multiple row types). These layouts can contain any View elements that a layout would normally contain.

Rows are created on demand by the adapter as they come onto the screen. The adapter is expected to either inflate a view of the appropriate type, or recycle the existing view and then customize it to display a row of data.

This “recycling” is a technique employed by the Android OS to improve performance. When new rows come onscreen, Android will pass into the adapter method the View of a row that has moved offscreen. It is up to the method to decide whether it’s appropriate to reuse that View to create the new row. For this to be the case, the View has to represent the layout of the new row. One way to check this is to write the layout ID into the Tag of each View inflated with setTag(). When checking to see whether it’s appropriate to reuse a given View, use getTag() to determine whether the View was inflated with the correct type. If an application is able to recycle a view, the scrolling appears smoother because CPU time is saved inflating the view.

Another way to make scrolling smoother is to do as little as possible on the UI thread. This is the default thread that your getView() method will be invoked on. If time-intensive operations need to be invoked, these can be done on a new background thread created especially for the operation. Then, when the UI thread is required again so that controls can be updated, operations can be invoked on it with activity.runOnUiThread(Runnable) or using a handler (see the discussions of inter-thread communication in Chapter 4). Care must be taken to ensure that the View to be modified has not been recycled for another row. This can happen if the row has moved off the screen in the time it took the operation to complete; this is quite possible if the operation was a lengthy download operation.

Setting up a basic ListView

Use the Eclipse New Android Project wizard to create a new Android project with a starting Activity called MainActivity. In the main.xml layout, replace the existing TextView section with the following:

<ListView android:id="@+id/ListView01"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"/>

At the bottom of the MainActivity.onCreate() method, insert the code shown in Example 8-2. This will declare a dummy anonymous class extending BaseAdapter, and apply an instance of it to the ListView. The code in Example 8-2 illustrates the methods that need to be supplied in order to populate the ListView with data.

Example 8-2. The adapter implementation
ListView listView = (ListView) findViewById(R.id.ListView01);
listView.setAdapter(new BaseAdapter(){

  public int getCount() {
    return 0;
  }

  public Object getItem(int position) {
    return null;
  }

  public long getItemId(int position) {
    return 0;
  }

  public View getView(int position, View convertView, ViewGroup parent) {
    return null;
  }
});

By customizing the anonymous class members, you can modify the data shown by the control. However, before any data can be shown, a layout must be supplied to present the data in rows. Add a file called list_row.xml to your project’s res/layout directory with the following content:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content" android:layout_height="wrap_content">
  <TextView android:text="@+id/TextView01" android:id="@+id/TextView01"
    android:layout_width="fill_parent" android:layout_height="wrap_content"/>
</LinearLayout>

In your MainActivity, add the following static array field containing just three strings:

static String[] words = {"one", "two", "three"};

Now customize your existing anonymous BaseAdapter as shown in Example 8-3, to display the contents of the words array in the ListView.

Example 8-3. The adapter implementation
listView.setAdapter(new BaseAdapter() {

  public int getCount() {
    return words.length;
  }

  public Object getItem(int position) {
    return words[position];
  }

  public long getItemId(int position) {
    return position;
  }

  public View getView(int position, View convertView, ViewGroup parent) {
    LayoutInflater inflater =
        (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
    View view = inflater.inflate(R.layout.list_row, null);
    TextView textView = (TextView) view.findViewById(R.id.TextView01);
    textView.setText(words[position]);
    return view;
  }
});

The getCount() method is customized to return the number of items in the list. Both getItem() and getItemId() supply the ListView with unique objects and IDs to identify the data in the rows. Finally, getView() creates and customizes an Android view to represent the row. This is the most complex step, so let’s break down what’s happening. First, the system LayoutInflater is obtained. This is the Service that creates views:

    LayoutInflater inflater =
        (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);

Next, the new layout we created earlier is inflated:

    View view = inflater.inflate(R.layout.list_row, null);

Then the TextView is located:

    TextView textView = (TextView) view.findViewById(R.id.TextView01);

and customized with the appropriate item in the words array:

    textView.setText(words[position]);

This allows the user to view elements from the words array in a ListView:

    return view;

Other recipes will discuss more details of ListView usage.

8.3 Creating a “No Data” View for ListViews

Rachee Singh

Problem

When a ListView has no items to show, the screen on an Android device is blank. You want to show an appropriate message, indicating the absence of data.

Solution

Use the “No Data” view from the XML layout.

Discussion

Often we need to use a ListView in an Android app. Before a user has loaded any data into the application, the list of data that the ListView shows is empty, generally resulting in a blank screen. In order to make the user feel more comfortable with the application, we might want to display an appropriate message (or even an image) stating that the list is empty. For this purpose, we can use a No Data view. This simply requires the addition of a few lines of code in the XML layout of the Activity that contains the ListView:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

<TextView
    android:id="@+id/textView1"
    android:text="@string/app_name"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"/>

<ListView
    android:id="@id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/textView1"/>
     <TextView
          android:id="@id/android:empty"
          android:text = "@string/list_is_empty"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:layout_below = "@id/textView1"
          android:textSize="25sp"
          android:gravity="center_vertical|center_horizontal"/>
 </RelativeLayout>

The important line is android:id="@id/android:empty". This line ensures that when the list is empty, the TextView with this ID will be displayed on the screen. In this TextView, the string “List is Empty” is displayed (see Figure 8-1).

ack2 0801
Figure 8-1. Empty list

A less important but interesting and relevant technique is the use of a RelativeLayout and the android:layout_below attribute to make the huge, empty text area appear directly below the tiny TextView at the top when the list is empty (effectively making the ListView and the “empty” message TextView the same size; only one will be visible at a time).

8.4 Creating an Advanced ListView with Images and Text

Marco Dinacci

Problem

You want to write a ListView that shows an image next to a string.

Solution

Create an Activity that inherits from ListActivity, prepare the XML resource files, and create a custom view adapter to load the resources into the view.

Discussion

The Android documentation says that the ListView widget is easy to use. This is true if you just want to display a simple list of strings, but as soon as you want to customize your list things become more complicated.

This recipe shows you how to write a ListView that displays a static list of images and strings, similar to the settings list on your phone. Figure 8-2 shows the final result.

ack2 0802
Figure 8-2. ListView with icons

Let’s start with the Activity code. First, we inherit from ListActivity instead of Activity so that we can easily supply our custom adapter (see Example 8-4).

Example 8-4. The ListActivity implementation
public class AdvancedListViewActivity extends ListActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Context ctx = getApplicationContext();
        Resources res = ctx.getResources();

        String[] options = res.getStringArray(R.array.country_names);
        TypedArray icons = res.obtainTypedArray(R.array.country_icons);

        setListAdapter(new ImageAndTextAdapter(ctx,
            R.layout.main_list_item, options, icons));
    }
}

In the onCreate() method we also create an array of strings, which contains the country names, and a TypedArray, which will contain our Drawable flags. The arrays are created from an XML file. Here is the content of the countries.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="country_names">
        <item>Bhutan</item>
        <item>Colombia</item>
        <item>Italy</item>
        <item>Jamaica</item>
        <item>Kazakhstan</item>
        <item>Kenya</item>
    </string-array>
    <array name="country_icons">
        <item>@drawable/bhutan</item>
        <item>@drawable/colombia</item>
        <item>@drawable/italy</item>
        <item>@drawable/jamaica</item>
        <item>@drawable/kazakhstan</item>
        <item>@drawable/kenya</item>
    </array>
</resources>

Now we’re ready to create the adapter. The official documentation for Adapter says:

An Adapter object acts as a bridge between an AdapterView and the underlying data for that view. The Adapter provides access to the data items. The Adapter is also responsible for making a View for each item in the data set.

There are several subclasses of Adapter; we’re going to extend ArrayAdapter, which is a concrete BaseAdapter that is backed by an array of arbitrary objects (see Example 8-5).

Example 8-5. The ImageAndTextAdapter class
public class ImageAndTextAdapter extends ArrayAdapter<String> {

    private LayoutInflater mInflater;

    private String[] mStrings;
    private TypedArray mIcons;

    private int mViewResourceId;

    public ImageAndTextAdapter(Context ctx, int viewResourceId,
            String[] strings, TypedArray icons) {
        super(ctx, viewResourceId, strings);

        mInflater = (LayoutInflater)ctx.getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);

        mStrings = strings;
        mIcons = icons;

        mViewResourceId = viewResourceId;
    }

    @Override
    public int getCount() {
        return mStrings.length;
    }

    @Override
    public String getItem(int position) {
        return mStrings[position];
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = mInflater.inflate(mViewResourceId, null);

        ImageView iv = (ImageView)convertView.findViewById(R.id.option_icon);
        iv.setImageDrawable(mIcons.getDrawable(position));

        TextView tv = (TextView)convertView.findViewById(R.id.option_text);
        tv.setText(mStrings[position]);

        return convertView;
    }
}

The constructor accepts a Context, the id of the layout that will be used for every row (more on this soon), an array of strings (the country names), and a TypedArray (our flags).

The getView() method is where we build a row for the list. We first use a LayoutInflater to create a View from XML, and then we retrieve the country flag as a Drawable and the country name as a String; we use them to populate the ImageView and TextView that we’ve declared in the layout. Here is the layout for the list rows:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
    <ImageView
    android:id="@+id/option_icon"
    android:layout_width="48dp"
    android:layout_height="fill_parent"/>
    <TextView
        android:id="@+id/option_text"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="10dp"
        android:textSize="16dp" >
    </TextView>
</LinearLayout>

And this is the content of the main layout:

<?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"
    >
    <ListView android:id="@android:id/list"
    android:layout_height="wrap_content"
    android:layout_width="fill_parent"
    />
</LinearLayout>

Note that the ListView ID must be exactly @android:id/list or you’ll get a RuntimeException.

Source Download URL

The source code for this example is in the Android Cookbook repository, in the subdirectory ListViewAdvanced (see “Getting and Using the Code Examples”).

8.5 Using Section Headers in ListViews

Wagied Davids

Problem

You want to display categorized items—for example, by time/day, by product category, or by sales/price.

Solution

Implement section headers yourself, using a custom list adapter, or use Jeff Sharkey’s implementation to display a list with headers.

Discussion

The “do it yourself” technique consists of creating a custom list adapter, and having its createView() method return either the standard layout or the header layout, depending on which layout is appropriate. This method will also work with the newer RecyclerView. Tutorials are available on using this approach with ListView and RecyclerView.

However, you may find it easier to use a packaged solution. Jeff Sharkey packaged his into a downloadable JAR file: the original section headers solution has been around since Android 0.9, which is basically the beginning of time. The intention was to duplicate the look of the standard Settings app, which at the time featured a look similar to the following image, which we will develop in this recipe:

ack2 08in01

The reusable part of this application is Jeff’s SeparatedListAdapter class, which implements the Composite design pattern by holding multiple adapters inside it and figuring out the correct one in its getItem() method.

We start with four XML files, one for the main layout (see Example 8-6) and three for the list entries. Jeff credited Romain Guy of Google with figuring out the built-in but rather occult styles used.

Example 8-6. 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">

    <ListView
        android:id="@+id/add_journalentry_menuitem"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
    <ListView
        android:id="@+id/list_journal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

The list_header layout (see Example 8-7) is used for the smaller list separators (e.g., “Security”).

Example 8-7. list_header.xml
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_header_title"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:paddingTop="2dip"
    android:paddingBottom="2dip"
    android:paddingLeft="5dip"
    style="?android:attr/listSeparatorTextViewStyle" />

The list_item and list_complex layouts are, of course, used for individual items (see Examples 8-8 and 8-9).

Example 8-8. list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_item_title"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:paddingTop="10dip"
    android:paddingBottom="10dip"
    android:paddingLeft="15dip"
    android:textAppearance="?android:attr/textAppearanceLarge"
    />
Example 8-9. list_complex.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="wrap_content"
    android:orientation="vertical"
    android:paddingTop="10dip"
    android:paddingBottom="10dip"
    android:paddingLeft="15dip"
    >
    <TextView
        android:id="@+id/list_complex_title"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        />
    <TextView
        android:id="@+id/list_complex_caption"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        />
</LinearLayout>

The add_journalentry_menuitem layout is used to add new entries, and is shown in action here (Example 8-10).

Example 8-10. add_journalentry_menuitem.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- list_item.xml -->
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_item_title"
    android:gravity="right"
    android:drawableRight="@drawable/ic_menu_add"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:paddingTop="0dip"
    android:paddingBottom="0dip"
    android:paddingLeft="10dip"
    android:textAppearance="?android:attr/textAppearanceLarge" />

Finally, Example 8-11 contains the Java Activity code.

Example 8-11. ListSample.java
public class ListSample extends Activity {

    public final static String ITEM_TITLE = "title";
    public final static String ITEM_CAPTION = "caption";

    // Section headers
    private final static String[] days =
        new String[]{"Mon", "Tue", "Wed", "Thur", "Fri"};

    // Section contents
    private final static String[] notes = new String[]
        {"Ate Breakfast", "Ran a Marathon ...yah really", "Slept all day"};

    // Menu - ListView
    private ListView addJournalEntryItem;

    // Adapter for ListView contents
    private SeparatedListAdapter adapter;

    // ListView contents
    private ListView journalListView;

    public Map<String, ?> createItem(String title, String caption) {
            Map<String, String> item = new HashMap<String, String>();
            item.put(ITEM_TITLE, title);
            item.put(ITEM_CAPTION, caption);
            return item;
        }

    @Override
    public void onCreate(Bundle icicle) {
            super.onCreate(icicle);

            // Sets the view layer
            setContentView(R.layout.main);

            // Interactive tools
            final ArrayAdapter<String> journalEntryAdapter =
              new ArrayAdapter<String>(this, R.layout.add_journalentry_menuitem,
              new String[]{"Add Journal Entry"});

            // addJournalEntryItem
            addJournalEntryItem = (ListView) this.findViewById(
                R.id.add_journalentry_menuitem);
            addJournalEntryItem.setAdapter(journalEntryAdapter);
            addJournalEntryItem.setOnItemClickListener(new OnItemClickListener() {
                  @Override
                  public void onItemClick(AdapterView<?> parent, View view,
                      int position, long duration) {
                          String item = journalEntryAdapter.getItem(position);
                          Toast.makeText(getApplicationContext(), item,
                              Toast.LENGTH_SHORT).show();
                      }
                });

            // Create the ListView adapter
            adapter = new SeparatedListAdapter(this);
            ArrayAdapter<String> listadapter = new ArrayAdapter<String>(this,
                R.layout.list_item, notes);

            // Add sections
            for (int i = 0; i < days.length; i++) {
                    adapter.addSection(days[i], listadapter);
            }

            // Get a reference to the ListView holder
            journalListView = (ListView) this.findViewById(R.id.list_journal);

            // Set the adapter on the ListView holder
            journalListView.setAdapter(adapter);

            // Listen for click events
            journalListView.setOnItemClickListener(new OnItemClickListener() {
                    @Override
                    public void onItemClick(AdapterView<?> parent, View view,
                        int position, long duration) {
                            String item = (String) adapter.getItem(position);
                            Toast.makeText(getApplicationContext(), item,
                                Toast.LENGTH_SHORT).show();
                        }
                });
        }

}

Unfortunately, we could not get copyright clearance from Jeff Sharkey to include the code, so you will have to download his SeparatedListAdapter, which ties all the pieces together; the link appears in the following “See Also” section.

See Also

Jeff’s original article on section headers.

Source Download URL

The source code for this example is in the Android Cookbook repository, in the subdirectory ListViewSectionedHeader (see “Getting and Using the Code Examples”).

8.6 Keeping the ListView with the User’s Focus

Ian Darwin

Problem

You don’t want to distract the user by moving the ListView to its beginning, away from what the user just did.

Solution

Keep track of the last thing you did in the List, and move the view there in onCreate().

Discussion

One of my biggest peeves is list-based applications that are always going back to the top of the list. Here are a few examples, some of which may have been fixed in recent years:

The standard Contacts manager

When you edit an item, it forgets about it and goes back to the top of the list.

The OpenIntents File Manager

When you delete an item from the bottom of a long list, it goes back to the top of the list to redisplay it, ignoring the fact that if I deleted an item, I may be cleaning up, and would like to keep working in the same area.

The HTC SenseUI for Tablets mail program

When you select a large number of emails using the per-message checkboxes and then delete them as one, it leaves the scrolling list in its previous position, which is now typically occupied by mail from yesterday or the day before!

It’s actually pretty simple to set the focus where you want it. Just find the item’s index in the adapter (using theList.getAdapter(), if needed), and then call:

theList.setSelection(index);

This will scroll to the given item and select it so that it becomes the default to act upon, though it doesn’t invoke the action associated with the item.

You can calculate this anyplace in your action code and pass it back to the main list view with Intent.putExtra(), or set it as a field in your main class and scroll the list in your onCreate() method or elsewhere.

8.7 Writing a Custom List Adapter

Alex Leffelman

Problem

You want to customize the content of a ListView.

Solution

In the Activity that will host your ListView, define a private class that extends Android’s BaseAdapter class. Then override the base class’s methods to display custom views that you define in an XML layout file.

Discussion

This code is lifted from a media application I wrote that allowed the user to build playlists from the songs on the SD card. We’ll extend the BaseAdapter class inside my MediaListActivity:

private class MediaAdapter extends BaseAdapter {
    ...
}

Querying the phone for the media info is outside the scope of this recipe, but the data to populate the list was stored in a MediaItem class that kept standard artist, title, album, and track number information, as well as a Boolean field indicating whether the item was selected for the current playlist. In certain cases, you may want to continually add items to your list—for example, if you’re downloading information and displaying it as it comes in—but in this recipe we’re going to supply all the required data to the adapter at once in the constructor:

public MediaAdapter(ArrayList<MediaItem> items) {
    mMediaList = items;
    ...
}

If you’re developing in Eclipse you’ll notice that it wants us to override BaseAdapter’s abstract methods; if you’re not, you’ll find this out as soon as you try to compile the code without them. Let’s take a look at those abstract methods:

public int getCount() {
    return mMediaList.size();
}

The framework needs to know how many views it needs to create in your list. It finds out by asking your adapter how many items you’re managing. In our case we’ll have a view for every item in the media list:

public Object getItem(int position) {
    return mMediaList.get(position);
}
public long getItemId(int position) {
    return position;
}

We won’t really be using these methods, but for completeness, getItem(int) is what gets returned when the ListView hosting this adapter calls getItemAtPosition(int), which won’t happen in our case. getItemId(int) is what gets passed to the ListView.onListItemClick(ListView, View, int, int) callback when you select an item. It gives you the position of the view in the list and the ID supplied by your adapter. In our case they’re the same.

The real work of your custom adapter will be done in the getView() method. This method is called every time the ListView brings a new item into view. When an item goes out of view, it is recycled by the system to be used later. This is a powerful mechanism for providing potentially thousands of View objects to our ListView while using only as many views as can be displayed on the screen. The getView() method provides the position of the item it’s creating, a view that may be not-null that the system is recycling for you to use, and the ViewGroup parent. You’ll return either a new view for the list to display, or a modified copy of the supplied convertView parameter to conserve system resources. Example 8-12 shows the code.

Example 8-12. The getView() method
public View getView(int position, View convertView, ViewGroup parent) {
    View V = convertView;

    if(V == null) {
        LayoutInflater vi =
            (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        V = vi.inflate(R.layout.media_row, null);
    }

    MediaItem mi = mMediaList.get(position);
    ImageView icon = (ImageView)V.findViewById(R.id.media_image);
    TextView title = (TextView)V.findViewById(R.id.media_title);
    TextView artist = (TextView)V.findViewById(R.id.media_artist);

    if(mi.isSelected()) {
        icon.setImageResource(R.drawable.item_selected);
    }
    else {
        icon.setImageResource(R.drawable.item_unselected);
    }

    title.setText(mi.getTitle());
    artist.setText("by " + mi.getArtist());

    return V;
}

We start by checking whether we’ll be recycling a view (which is a good practice) or need to generate a new view from scratch. If we weren’t given a convertView, we’ll call the LayoutInflater Service to build a view that we’ve defined in an XML layout file.

Using the view that we’ve ensured was built with our desired layout resource (or is a recycled copy of one we previously built), it’s simply a matter of updating its UI elements. In our case, we want to display the song title, artist, and an indication of whether the song is in our current playlist. (I’ve removed the error checking, but it’s a good practice to make sure any UI elements you’re updating are not null—you don’t want to crash the whole ListView if there was a small mistake in one item.) This method gets called for every (visible) item in the ListView, so in this example we have a list of identical View objects with different data being displayed in each one. If you wanted to get really creative, you could populate the list with different view layouts based on the list item’s position or content.

That takes care of the required BaseAdapter overrides. However, you can add any functionality to your adapter to work on the data set it represents. In my example, I want the user to be able to click a list item and toggle it on/off for the current playlist. This is easily accomplished with a simple callback on the ListView and a short function in the adapter.

This function belongs to ListActivity:

protected void onListItemClick(ListView l, View v, int position, long id) {
    super.onListItemClick(l, v, position, id);

    mAdapter.toggleItem(position);
}

This is a member function in our MediaAdapter:

public void toggleItem(int position) {
    MediaItem mi = mMediaList.get(position);

    mi.setSelected(!mi.getSelected());
    mMediaList.set(position, mi);

    this.notifyDataSetChanged();
}

First we simply register a callback for when the user clicks an item in our list. We’re given the ListView, the View, the position, and the ID of the item that was clicked, but we’ll only need the position, which we simply pass to the MediaAdapter.toggleItem(int) method. In that method we update the state of the corresponding MediaItem and make an important call to notifyDataSetChanged(). This method lets the framework know that it needs to redraw the ListView. If we don’t call it, we can do whatever we want to the data, but we won’t see anything change until the next redraw (e.g., when we scroll the list).

When all is said and done, we need to tell the parent ListView to use our adapter to populate the list. That’s done with a simple call in the ListActivity’s onCreate(Bundle) method:

MediaAdapter mAdapter = new MediaAdapter(getSongsFromSD());
this.setListAdapter(mAdapter);

First we instantiate a new adapter with data generated from a private function that queries the phone for the song data, and then we tell the ListActivity to use that adapter to draw the list. And there it is—your own list adapter with a custom view and extensible functionality.

8.8 Using a SearchView to Search Through Data in a ListView

Ian Darwin

Problem

You want a search box (with a magnifying glass icon and an X to clear the text) to filter the content displayed in a ListView.

Solution

Use a SearchView in your layout. Call setTextFilterEnabled(true) on the ListView. Call setOnQueryTextListener() on the SearchView, passing an instance of SearchView.OnQueryTextListener. In the listener’s onQueryTextChanged() method, pass the argument along to the ListView’s setFilterText(), and you’re done!

Discussion

SearchView is a powerful control that is often overlooked in designing list applications. It has two modes: inline and Activity-based. In the inline mode, which we’ll demonstrate here, the SearchView can be added to an existing list-based application with minimal disruption. In the Activity-based mode, a separate Activity must be created to display the results; that is not covered here but is in the official documentation.

The ListView class has a filter mechanism built in. If you enable it and call setFilterText("string"), then only items in the list that contain +string+ will be visible. You can call this many times, e.g., as each character is typed, for a dynamic effect.

Assuming you have a working ListView-based application, you need to take the following steps:

  1. Add a SearchView to the layout file and give it an id such as searchView.

  2. In your list Activity’s onCreate() method, find the ListView if needed, and call its setTextFilterEnabled(true) method.

  3. In your list Activity’s onCreate() method, find the SearchView by ID and call several methods on it, the important one being setOnQueryTextListener().

  4. In the QueryTextListener’s onQueryTextChanged() method, if the passed CharSequence is empty, clear the ListView’s filter text (so it will display all the entries). If the passed CharSequence is not empty, convert it to a String and pass it to the ListView’s setFilterText() method.

It really is that simple! The following code snippets are taken from a longer ListView example that was made to work with a SearchView by following this recipe.

In the Activity’s onCreate() method:

// Tailor the adapter for the SearchView
mListView.setTextFilterEnabled(true);

// Tailor the SearchView
mSearchView = (SearchView) findViewById(R.id.searchView);
mSearchView.setIconifiedByDefault(false);
mSearchView.setOnQueryTextListener(this);
mSearchView.setSubmitButtonEnabled(false);
mSearchView.setQueryHint(getString(R.string.search_hint));

And in the Activity’s implementation of SearchView.OnQueryTextListener:

public boolean onQueryTextChange(String newText) {
    if (TextUtils.isEmpty(newText)) {
        mListView.clearTextFilter();
    } else {
        mListView.setFilterText(newText.toString());
    }
    return true;
}

public boolean onQueryTextSubmit(String query) {
    return false;
}

The ListView does the work of filtering; we just need to control its filtering using the two methods called from onQueryTextChange(). There is a lot more to the SearchView, though; consult the Javadoc page or the developer documentation for more information.

See Also

The Android training guide on adding search functionality, the developer documentation on SearchView.

Source Download URL

This code is in the MainActivity class of TodoAndroid, a simple to-do list manager.

8.9 Handling Orientation Changes: From ListView Data Values to Landscape Charting

Wagied Davids

Problem

You want to react to orientation changes in layout-appropriate ways. For example, data values to be plotted are contained in a portrait list view, and upon device rotation to landscape, a graph of the data values in a chart/plot is displayed.

Solution

Do something in reaction to physical device orientation changes. A new View object is created on orientation changes. You can override the Activity method onConfig⁠urationChan⁠ged(Config⁠uration newConf⁠ig) to accommodate orientation changes.

Discussion

In this recipe, data values to be plotted are contained in a portrait list view. When the device/emulator is rotated to landscape, a new Intent is launched to change to a plot/charting View to graphically display the data values. Charting is accomplished using the free DroidCharts package.

Note that for testing this in the Android emulator, the Ctrl-F11 key combination will result in a portrait to landscape (or vice versa) orientation change.

The most important trick is to modify the AndroidManifest.xml file (shown in Example 8-13) to allow for the following:

android:configChanges="orientation|keyboardHidden"
android:screenOrientation="portrait"
Example 8-13. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples"
    android:versionCode="1"
    android:versionName="1.0">
    <application
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity
            android:name=".DemoList"
            android:label="@string/app_name"
            android:configChanges="orientation|keyboardHidden"
            android:screenOrientation="portrait">
            <intent-filter>
                <action
                    android:name="android.intent.action.MAIN" />
                <category
                    android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".DemoCharts"
            android:configChanges="orientation|keyboardHidden"></activity>
    </application>
</manifest>

The main Activity in this example is DemoCharts, shown in Example 8-14. It does the usual onCreate() stuff, but also—if a parameter was passed—it assumes our app was restarted from the DemoList class shown in Example 8-15 and sets up the data accordingly. (A number of methods have been elided here as they aren’t relevant to the core issue, that of configuration changes. These are in the online source for this recipe.)

Example 8-14. DemoCharts.java
...
import net.droidsolutions.droidcharts.core.data.XYDataset;
import net.droidsolutions.droidcharts.core.data.xy.XYSeries;
import net.droidsolutions.droidcharts.core.data.xy.XYSeriesCollection;

public class DemoCharts extends Activity {
        private static final String tag = "DemoCharts";
        private final String chartTitle = "My Daily Starbucks Allowance";
        private final String xLabel = "Week Day";
        private final String yLabel = "Allowance";

        /** Called when the Activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Access the Extras from the Intent
            Bundle params = getIntent().getExtras();

            // If we get no parameters, we do nothing
            if (params == null) { return; }

            // Get the passed parameter values
            String paramVals = params.getString("param");

            Log.d(tag, "Data Param:= " + paramVals);
            Toast.makeText(getApplicationContext(), "Data Param:= " +
                paramVals, Toast.LENGTH_LONG).show();

            ArrayList<ArrayList<Double>> dataVals = stringArrayToDouble(paramVals);

                XYDataset dataset =
                    createDataset("My Daily Starbucks Allowance", dataVals);
                XYLineChartView graphView = new XYLineChartView(this, chartTitle,
                    xLabel, yLabel, dataset);
                setContentView(graphView);
        }

        private String arrayToString(String[] data) {
            ...
        }

        private ArrayList<ArrayList<Double>> stringArrayToDouble(String paramVals) {
            ...
        }

        /**
         * Creates a sample data set.
         */
        private XYDataset createDataset(String title,
                ArrayList<ArrayList<Double>> dataVals) {

                final XYSeries series1 = new XYSeries(title);
                for (ArrayList<Double> tuple : dataVals) {
                        double x = tuple.get(0).doubleValue();
                        double y = tuple.get(1).doubleValue();

                        series1.add(x, y);
                    }

                // Create a collection to hold various data sets
                final XYSeriesCollection dataset = new XYSeriesCollection();
                dataset.addSeries(series1);
                return dataset;
            }

        @Override
        public void onConfigurationChanged(Configuration newConfig) {
                super.onConfigurationChanged(newConfig);
                Toast.makeText(this, "Orientation Change", Toast.LENGTH_SHORT);

                // Let's go to our DemoList view
                Intent intent = new Intent(this, DemoList.class);
                startActivity(intent);

                // Finish current Activity
                this.finish();
            }
    }

The DemoList view is the portrait view. Its onConfigurationChanged() method passes control back to the landscape DemoCharts class if a configuration change occurs.

Example 8-15. DemoList.java
public class DemoList extends ListActivity implements OnItemClickListener {
        private static final String tag = "DemoList";
        private ListView listview;
        private ArrayAdapter<String> listAdapter;

        // Want to pass data values as parameters to next Activity/View/Page
        private String params;

        // Our data for plotting
        private final double[][] data = {
             { 1, 1.0 }, { 2.0, 4.0 }, { 3.0, 10.0 }, { 4, 2.0 },
             { 5.0, 20 }, { 6.0, 4.0 }, { 7.0, 1.0 },
        };

        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);

                // Set the view layer
                setContentView(R.layout.data_listview);

                // Get the default declared ListView @android:list
                listview = getListView();

                // List for click events to the ListView items
                listview.setOnItemClickListener(this);

                // Get the data
                ArrayList<String> dataList = getDataStringList(data);

                // Create an adapter for viewing the ListView
                listAdapter = new ArrayAdapter<String>(this,
                    android.R.layout.simple_list_item_1, dataList);

                // Bind the adapter to the ListView
                listview.setAdapter(listAdapter);

                // Set the parameters to pass to the next view/page
                setParameters(data);
        }

        private String doubleArrayToString(double[][] dataVals) {
                ...
        }

        /**
         * Sets parameters for the Bundle
         * @param dataList
         */
        private void setParameters(double[][] dataVals) {
                params = toJSON(dataVals);
        }

        public String getParameters() {
                return this.params;
        }

        /**
         * @param dataVals
         * @return
         */
        private String toJSON(double[][] dataVals) {
                ...
        }

        private ArrayList<String> getDataStringList(double[][] dataVals) {
                ...
        }

        @Override
        public void onConfigurationChanged(Configuration newConfig) {
                super.onConfigurationChanged(newConfig);

                // Create an Intent to switch view to the next page view
                Intent intent = new Intent(this, DemoCharts.class);

                // Pass parameters along to the next page
                intent.putExtra("param", getParameters());

                // Start the Activity
                startActivity(intent);

                Log.d(tag, "Orientation Change...");
                Log.d(tag, "Params: " + getParameters());
        }

        @Override
        public void onItemClick(AdapterView<?> parent, View view,
                int position, long duration) {

            // Upon clicking item in list, pop up a toast
            String msg = "#Item: " + String.valueOf(position) +
                " - " + listAdapter.getItem(position);
            Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
        }
}

The XYLineChartView class is not included here as it relates only to the plotting. It is included in the online version of the code, which you can download.

Source Download URL

The source code for this example is in the Android Cookbook repository, in the subdirectory OrientationChanges (see “Getting and Using the Code Examples”).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.238.227.73