160

On Android, how can I a ListView that filters based on user input, where the items shown are updated dynamically based on the TextView value?

I'm looking for something like this:

-------------------------
| Text View             |
-------------------------
| List item             |
| List item             |
| List item             |
| List item             |
|                       |
|                       |
|                       |
|                       |
-------------------------
Hamy
  • 20,662
  • 15
  • 74
  • 102
  • 8
    I nominate for re-opening. I've updated the text to make this more of a question, and it's clearly a valuable community resource as answers are still coming in – Hamy Sep 20 '14 at 18:20
  • Please reopen the question. It is clearly helpful. – SilentNot Oct 19 '17 at 15:10

3 Answers3

287

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);
}
Janusz
  • 187,060
  • 113
  • 301
  • 369
Hamy
  • 20,662
  • 15
  • 74
  • 102
  • 7
    Is there any straightforward way to filter ListView in "contains" instead of "starts with" fashion like this solution does? – Viktor Brešan Sep 18 '10 at 18:49
  • 12
    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 Sep 20 '10 at 02:13
  • what is getStringArrayList(),i run this code,but cannot get the filter effect,can you help me – pengwang Dec 02 '10 at 10:20
  • I have implemented your solution but I am not getting my text filtered and I dont even get any exceptions. Please help. – hassanadnan Mar 31 '11 at 06:57
  • same here..not able to filter.. is some listener missing – Vicky Kapadia Mar 31 '11 at 11:33
  • 2
    Hamy, excellent writeup! I have a question though: I have implemented this and after each letter that I type, the ListView disappears for a couple of seconds and then comes back, filtered. Have you experienced this? My intuition is that it's because I have 600+ items in the list, with non-trivial toString() functions. – lowellk Apr 23 '11 at 01:17
  • To disable the default suggestions in the text box, you should use android:inputType="textFilter" instead of "text". – Martin Konicek Jun 26 '11 at 12:06
  • @lowellk - This solution is unusably slow for 1000 items on HTC Wildfire. I think it is usable AS-IS only for short lists. – Martin Konicek Jun 26 '11 at 12:07
  • 2
    In landscape mode on a smaller screen, the EditText + keyboard takes the whole screen and the ListView is not visible! Any solution? – Martin Konicek Jul 07 '11 at 13:06
  • @Hamy what is getStringArrayList() in your solution? – Krishnabhadra Aug 10 '11 at 09:06
  • To understand the 'getStringArrayList()' function, just take a peek at the ArrayAdapter documentation - see http://developer.android.com/reference/android/widget/ArrayAdapter.html . The function either returns a String[], or a List. You can be fancy and do items other than Strings, but that's more advanced than I'm covering here. The strings should be the text that you want to show up in the listview, one string per row naturally – Hamy Aug 17 '11 at 02:30
  • Hamy, have you run into problems with this when rotating your device? I'm experiencing mysterious crashes (throwing weird exceptions) when I have text entered and rotate the device. Any idea? – riha Sep 14 '11 at 13:32
  • Dont work on DB inflated ListViews. :) – Leandros Jan 27 '12 at 21:38
  • Adapter.getFilter().setFilter(....) along with runQueryOnBackgroundThread(CharSequence) works great, thanks a lot :) – Soham Jan 10 '13 at 19:09
  • 4
    Is this really necessary? > Remember to remove the TextWatcher in OnDestroy() – Jojo Feb 25 '13 at 07:56
  • @Hamy i had use in this same way now problmen is that when user click on Any one item i want to display that item in full view. so basicaly i m loop arraylist and find compare text of getItemPosition text and arraylist but in my arraylist there are many thing of same name then how can i idetify that this name is selected? – PankajAndroid Aug 14 '13 at 06:19
  • getFilter method returns null for me. What can be the problem? – user3104760 May 21 '16 at 08:37
  • @Hamy Sir Your Code is too much wastage of time You are waste my time also . Thanks For wasting My time – Ashish Shahi Apr 26 '17 at 11:58
10

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?

Janusz
  • 187,060
  • 113
  • 301
  • 369
j7nn7k
  • 17,995
  • 19
  • 78
  • 88
  • 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 Feb 08 '10 at 15:23
  • 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. – j7nn7k Feb 11 '10 at 06:36
  • 1
    The forceclose occorrs when starting to type in. Part of the error: ERROR/AndroidRuntime(188): java.lang.NullPointerException 02-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) – j7nn7k Feb 11 '10 at 06:38
  • 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 Feb 18 '10 at 14:42
4

i had a problem with filtering, that results have been filtered, but not restored!

so before filtering (activity start) i created a list backup.. (just another list, containing the same data)

on filtering, the filter and listadapter is connected to the primary list.

but the filter itself used the data from the backuped list.

this ensured in my case, that the list was updated immediately and even on deleting search-term-characters the list gets restored successfully in every case :)

thanks for this solution anyways.

cV2
  • 5,229
  • 3
  • 43
  • 53