views:

5078

answers:

2

I want a nice looking filter for my ListView in Android.

How can I do this?

+34  A: 

First, you need to create an XML layout that has both an EditText, and a ListView.

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <!-- Pretty hint text, and maxLines -->
    <EditText android:id="@+building_list/search_box" 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:hint="type to filter"
        android:inputType="text"
        android:maxLines="1"/>

    <!-- Set height to 0, and let the weight param expand it -->
    <!-- Note the use of the default ID! This lets us use a 
         ListActivity still! -->
    <ListView android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="0dip"
        android:layout_weight="1" 
         /> 

</LinearLayout>

This will lay everything out properly, with a nice EditText above the ListView. Next, create a ListActivity as you would normally, but add a setContentView() call in the onCreate() method so we use our recently declared layout. Remember that we ID'ed the ListView specially, with android:id="@android:id/list". This allows the ListActivity to know which ListView we want to use in our declared layout.

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

        setContentView(R.layout.filterable_listview);

        setListAdapter(new ArrayAdapter<String>(this,
                       android.R.layout.simple_list_item_1, 
                       getStringArrayList());
    }

Running the app now should show your previous ListView, with a nice box above. In order to make that box do something, we need to take the input from it, and make that input filter the list. While a lot of people have tried to do this manually, most ListView Adapter classes come with a Filter object that can be used to perform the filtering automagically. We just need to pipe the input from the EditText into the Filter. Turns out that is pretty easy. To run a quick test, add this line to your onCreate() call

adapter.getFilter().filter(s);

Notice that you will need to save your ListAdapter to a variable to make this work - I have saved my ArrayAdapter<String> from earlier into a variable called 'adapter'.

Next step is to get the input from the EditText. This actually takes a bit of thought. You could add an OnKeyListener() to your EditText. However, this listener only receives some key events. For example, if a user enters 'wyw', the predictive text will likely recommend 'eye'. Until the user chooses either 'wyw' or 'eye', your OnKeyListener will not receive a key event. Some may prefer this solution, but I found it frustrating. I wanted every key event, so I had the choice of filtering or not filtering. The solution is a TextWatcher. Simply create and add a TextWatcher to the EditText, and pass the ListAdapter Filter a filter request every time the text changes. Remember to remove the TextWatcher in OnDestroy()! Here is the final solution:

private EditText filterText = null;
ArrayAdapter<String> adapter = null;

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

    setContentView(R.layout.filterable_listview);

    filterText = (EditText) findViewById(R.id.search_box);
    filterText.addTextChangedListener(filterTextWatcher);

    setListAdapter(new ArrayAdapter<String>(this,
                   android.R.layout.simple_list_item_1, 
                   getStringArrayList());
}

private TextWatcher filterTextWatcher = new TextWatcher() {

    public void afterTextChanged(Editable s) {
    }

    public void beforeTextChanged(CharSequence s, int start, int count,
            int after) {
    }

    public void onTextChanged(CharSequence s, int start, int before,
            int count) {
        adapter.getFilter().filter(s);
    }

};

@Override
protected void onDestroy() {
    super.onDestroy();
    filterText.removeTextChangedListener(filterTextWatcher);
}
Hamy
Is there any straightforward way to filter ListView in "contains" instead of "starts with" fashion like this solution does?
Viktor Bresan
Viktor - If the words you are interested in are separated by spaces, then it will do this automatically. Otherwise, not really. Probably the easiest way would be to subclass the Adapter by extending it, and override the getFilter method to return a Filter object that you define. See http://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ArrayAdapter.java#L449 to understand how the default ArrayFilter works - it would be simple to copy 95% of this code and change lines 479 and 486
Hamy
+1  A: 

running the programm will cause a force close.

I swaped the line:

android:id="@+building_list/search_box"

with

android:id="@+id/search_box"

could that be the problem? What is the '@+building_list' for?

Johe Green
Johe, when you say R.id.something, the something exists in id because you said android:id="@+id/search_box". If you say android:id="@+building_list/search_box" then in the code you can call findViewById(R.building_list.search_box);What is the top level exception you are getting? This code is copied w/o a test compile, so I likely left at least one error in there somewhere
Hamy
Hi Hamy, you referenced "@+building_list/search_box" in the code with "filterText = (EditText) findViewById(R.id.search_box);"That's why I was wondering.
Johe Green
The forceclose occorrs when starting to type in. Part of the error:ERROR/AndroidRuntime(188): java.lang.NullPointerException02-11 07:30:29.828: ERROR/AndroidRuntime(188): at xxx.com.ListFilter$1.onTextChanged(ListFilter.java:46)02-11 07:30:29.828: ERROR/AndroidRuntime(188): at android.widget.TextView.sendOnTextChanged(TextView.java:6102)02-11 07:30:29.828: ERROR/AndroidRuntime(188): at android.widget.TextView.handleTextChanged(TextView.java:6143)02-11 07:30:29.828: ERROR/AndroidRuntime(188): at android.widget.TextView$ChangeWatcher.onTextChanged(TextView.java:6286)
Johe Green
Johe, Oops - it looks like I was trying to modify the code to get rid of the @+building_list (because that's a point of confusion) but I didnt catch it everywhere. Thanks for the tip! Just changing it to @+id should fix it
Hamy