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
.
Ian Darwin
Use a RecyclerView
.
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.
<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; } }
The developer documentation on RecyclerView
.
Jim Blackler
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.
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.
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.
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
.
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.
Rachee Singh
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).
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).
Marco Dinacci
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.
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.
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).
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 aView
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).
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 Runtime
Exception
.
The source code for this example is in the Android Cookbook repository, in the subdirectory ListViewAdvanced (see “Getting and Using the Code Examples”).
Wagied Davids
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:
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.
<?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”).
<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).
<?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"
/>
<?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).
<?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.
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.
Jeff’s original article on section headers.
The source code for this example is in the Android Cookbook repository, in the subdirectory ListViewSectionedHeader (see “Getting and Using the Code Examples”).
Ian Darwin
Keep track of the last thing you did in the List
, and move the view there in onCreate()
.
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:
When you edit an item, it forgets about it and goes back to the top of the list.
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.
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.
Alex Leffelman
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.
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.
Ian Darwin
Use a SearchView
in your layout. Call setTextFilterEnabled(true)
on the ListView
. Call set
OnQueryTextListener()
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!
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:
Add a SearchView
to the layout file and give it an id
such as searchView
.
In your list Activity’s onCreate()
method, find the ListView
if needed, and call its setTextFilterEnabled(true)
method.
In your list Activity’s onCreate()
method, find the SearchView
by ID and call several methods on it, the important one being setOnQueryTextListener()
.
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.
The Android training guide on adding search functionality, the developer documentation on SearchView
.
This code is in the MainActivity
class of TodoAndroid, a simple to-do list manager.
Wagied Davids
Do something in reaction to physical device orientation changes. A new View
object is created on orientation changes. You can override the Activity
method onConfigurationChanged(Configuration newConfig)
to accommodate orientation changes.
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"
<?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.)
...
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.
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.
The source code for this example is in the Android Cookbook repository, in the subdirectory OrientationChanges (see “Getting and Using the Code Examples”).
3.238.227.73