How to write a custom filter for ListView with ArrayAdapter

Actually

I noticed that I should have been using ‘originalItems’ list to build the new filtered one in performFiltering.

This will fix any issues that you see regarding changing the text in the filter. E.g. you search for ‘Bread’ then backspace to just a ‘B’ and you should see all ‘B’s. In my original post you would not have.

    private class GlycaemicIndexItemAdapter extends ArrayAdapter<GlycaemicIndexItem> {

    private ArrayList<GlycaemicIndexItem> items;
    private ArrayList<GlycaemicIndexItem> originalItems = new ArrayList<GlycaemicIndexItem>();
    private GlycaemicIndexItemFilter filter;
    private final Object mLock = new Object();

    public GlycaemicIndexItemAdapter(Context context, int textViewResourceId, ArrayList<GlycaemicIndexItem> newItems) {
            super(context, textViewResourceId, newItems);
            this.items = newItems;
            cloneItems(newItems);
    }

    protected void cloneItems(ArrayList<GlycaemicIndexItem> items) {
        for (Iterator iterator = items.iterator(); iterator
        .hasNext();) {
            GlycaemicIndexItem gi = (GlycaemicIndexItem) iterator.next();
            originalItems.add(gi);
        }
    }

    @Override
    public int getCount() {
        synchronized(mLock) {
            return items!=null ? items.size() : 0;  

    }

    @Override
    public GlycaemicIndexItem getItem(int item) {
        GlycaemicIndexItem gi = null;
        synchronized(mLock) {
                gi = items!=null ? items.get(item) : null;

        }
        return gi;
    }

    @Override
    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.row, null);
            }

            GlycaemicIndexItem i  = null;
            synchronized(mLock) {
                i = items.get(position);
            }

            if (i != null) {
                    TextView tt = (TextView) v.findViewById(R.id.rowText);
                    TextView bt = (TextView) v.findViewById(R.id.rowText2);
                    if (tt != null) {
                          tt.setText("Name: "+i.getName());                            
                    }
                    if(bt != null){
                          bt.setText("GI Value: " + i.getGlycaemicIndex());
                    }
            }
            return v;
    }
    /**
     * Implementing the Filterable interface.
     */
    public Filter getFilter() {
        if (filter == null) {
            filter = new GlycaemicIndexItemFilter();
        }
        return filter;
    }   

    /**
     * Custom Filter implementation for the items adapter.
     *
     */
    private class GlycaemicIndexItemFilter extends Filter {
        protected FilterResults performFiltering(CharSequence prefix) {
            // Initiate our results object
            FilterResults results = new FilterResults();

            // No prefix is sent to filter by so we're going to send back the original array
            if (prefix == null || prefix.length() == 0) {
                synchronized (mLock) {
                    results.values = originalItems;
                    results.count = originalItems.size();
                }
            } else {
                synchronized(mLock) {
                        // Compare lower case strings
                    String prefixString = prefix.toString().toLowerCase();
                    final ArrayList<GlycaemicIndexItem> filteredItems = new ArrayList<GlycaemicIndexItem>();
                    // Local to here so we're not changing actual array
                    final ArrayList<GlycaemicIndexItem> localItems = new ArrayList<GlycaemicIndexItem>();
                    localItems.addAll(originalItems);
                    final int count = localItems.size();

                    for (int i = 0; i < count; i++) {
                        final GlycaemicIndexItem item = localItems.get(i);
                        final String itemName = item.getName().toString().toLowerCase();

                        // First match against the whole, non-splitted value
                        if (itemName.startsWith(prefixString)) {
                            filteredItems.add(item);
                        } else {} /* This is option and taken from the source of ArrayAdapter
                            final String[] words = itemName.split(" ");
                            final int wordCount = words.length;

                            for (int k = 0; k < wordCount; k++) {
                                if (words[k].startsWith(prefixString)) {
                                    newItems.add(item);
                                    break;
                                }
                            }
                        } */
                    }

                    // Set and return
                    results.values = filteredItems;
                    results.count = filteredItems.size();
                }//end synchronized
            }

            return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence prefix, FilterResults results) {
            //noinspection unchecked
            synchronized(mLock) {
                final ArrayList<GlycaemicIndexItem> localItems = (ArrayList<GlycaemicIndexItem>) results.values;
                notifyDataSetChanged();
                clear();
                //Add the items back in
                for (Iterator iterator = localItems.iterator(); iterator
                        .hasNext();) {
                    GlycaemicIndexItem gi = (GlycaemicIndexItem) iterator.next();
                    add(gi);
                }
            }//end synchronized
        }
    }
}

Basically I am building a health and nutrition application and one screen will have a list of items based on the glycaemic/glycemic index. I want users to be able to type and have the screen autofilter. Now, if you are only using strings, you get autofiltering for free. I am not though, I have my own custom class GlycaemicIndexItem which has properties on it. I need to provide my own filtering to ensure that the list used to be drawn on screen is updated when the user types.

Currently the screen is a simple ListActivity, with a ListView and an EditText (which the user types in). We will attach a TextWatcher to this EditText to ensure that we are notified of updates to it. This means that it should work for all devices regardless of the user typing on a hard or soft keyboard (I have an HTC DesireZ and an old G1).

Here is the layout xml for the screen/activity (Can someone tell me how to paste xml code into here, as when I try to use code block xml does not get pasted/displayed properly, but interpreted):

layout for the activity - giatoz.xml

As we want to display our rows in custom style, we also have a layout xml file for the row itself:
Row xml file

Here is the code for the entire Activity itself. Extending from ListActivity, this class has an inner class that acts as the adapter, which extends from ArrayAdapter. This is instantiated in the onCreate of the Activity and passed a simple list of strings for now. Pay attention to how it is created on lines 39-40. Our special layout for the row is passed in with the list of items.

The key to populating the custom rows is in the adapter’s method getView.

Our adapter class also has its own inner class called GlycaemicIndexItemFilter which does the work when a user types. Our filter is bound to our EditText on lines 43-44 by use of a TextWatcher and its method afterTextChanged. The line 47 is the clue as to how we achieve filtering. We call filter, on our filter object. Our filter is created when we call getFilter the first time, line 148-149.

   package com.tilleytech.android.myhealthylife;

     import java.util.ArrayList;
     import java.util.Iterator;

     import android.app.ListActivity;
      import android.content.Context;
     import android.content.res.Resources;
     import android.os.Bundle;
     import android.text.Editable;
      import android.text.TextWatcher;
      import android.view.LayoutInflater;
        import android.view.View;
       import android.view.ViewGroup;
       import android.widget.ArrayAdapter;
       import android.widget.EditText;
       import android.widget.Filter;
       import android.widget.ListView;
       import android.widget.TextView;


        public class GlycaemicIndexAtoZActivity extends ListActivity {
          /** Called when the activity is first created. */
        private GlycaemicIndexItemAdapter giAdapter; 
        private TextWatcher filterTextWatcher;
        private EditText filterText = null;

        @Override
        public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.giatoz);            

        ListView lv = getListView();
        lv.setTextFilterEnabled(true);
        // By using setAdapter method in listview we an add string array in list.
        ArrayList<GlycaemicIndexItem> list = getListItems();

        giAdapter = new GlycaemicIndexItemAdapter(this, R.layout.row, list);
        giAdapter.notifyDataSetChanged();
        setListAdapter(giAdapter);

        filterText = (EditText)findViewById(R.id.GI_AtoZSearchEditText);
        filterTextWatcher = new TextWatcher() {

            public void afterTextChanged(Editable s) {
                giAdapter.getFilter().filter(s); //Filter from my adapter
                giAdapter.notifyDataSetChanged(); //Update my view

            }

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

            public void onTextChanged(CharSequence s, int start, int before,
                    int count) {

            }

        };
        filterText.addTextChangedListener(filterTextWatcher);
    }

    private ArrayList<GlycaemicIndexItem> getListItems() {
        ArrayList<GlycaemicIndexItem> result = new ArrayList<GlycaemicIndexItem>();

        Resources res = getResources();
        //Get our raw strings
        String[] array = res.getStringArray(R.array.GIList);
        for (int i = 0; i < array.length; i++) {
            GlycaemicIndexItem gi = new GlycaemicIndexItem();
            gi.setName(array[i]);
            gi.setGlycaemicIndex(1);
            result.add(gi);
        }

        return result;
    }

    private class GlycaemicIndexItemAdapter extends ArrayAdapter<GlycaemicIndexItem> {

        private ArrayList<GlycaemicIndexItem> items;
        private ArrayList<GlycaemicIndexItem> originalItems = new ArrayList<GlycaemicIndexItem>();
        private GlycaemicIndexItemFilter filter;
        private final Object mLock = new Object();

        public GlycaemicIndexItemAdapter(Context context, int textViewResourceId, ArrayList<GlycaemicIndexItem> newItems) {
                super(context, textViewResourceId, newItems);
                this.items = newItems;
                cloneItems(newItems);
        }

        protected void cloneItems(ArrayList<GlycaemicIndexItem> items) {
            for (Iterator iterator = items.iterator(); iterator
            .hasNext();) {
                GlycaemicIndexItem gi = (GlycaemicIndexItem) iterator.next();
                originalItems.add(gi);
            }
        }

        @Override
        public int getCount() {
            synchronized(mLock) {
                return items!=null ? items.size() : 0;  
            }
        }

        @Override
        public GlycaemicIndexItem getItem(int item) {
            GlycaemicIndexItem gi = null;
            synchronized(mLock) {
                    gi = items!=null ? items.get(item) : null;

            }
            return gi;
        }

        @Override
        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.row, null);
                }

                GlycaemicIndexItem i  = null;
                synchronized(mLock) {
                    i = items.get(position);
                }

                if (i != null) {
                        TextView tt = (TextView) v.findViewById(R.id.rowText);
                        TextView bt = (TextView) v.findViewById(R.id.rowText2);
                        if (tt != null) {
                              tt.setText("Name: "+i.getName());                            
                        }
                        if(bt != null){
                              bt.setText("GI Value: " + i.getGlycaemicIndex());
                        }
                }
                return v;
        }
        /**
         * Implementing the Filterable interface.
         */
        public Filter getFilter() {
            if (filter == null) {
                filter = new GlycaemicIndexItemFilter();
            }
            return filter;
        }   

        /**
         * Custom Filter implementation for the items adapter.
         *
         */
        private class GlycaemicIndexItemFilter extends Filter {
            protected FilterResults performFiltering(CharSequence prefix) {
                // Initiate our results object
                FilterResults results = new FilterResults();

                // No prefix is sent to filter by so we're going to send back the original array
                if (prefix == null || prefix.length() == 0) {
                    synchronized (mLock) {
                        results.values = originalItems;
                        results.count = originalItems.size();
                    }
                } else {
                    synchronized(mLock) {
                            // Compare lower case strings
                        String prefixString = prefix.toString().toLowerCase();
                        final ArrayList<GlycaemicIndexItem> filteredItems = new ArrayList<GlycaemicIndexItem>();
                        // Local to here so we're not changing actual array
                        final ArrayList<GlycaemicIndexItem> localItems = new ArrayList<GlycaemicIndexItem>();
                        localItems.addAll(originalItems);
                        final int count = localItems.size();

                        for (int i = 0; i < count; i++) {
                            final GlycaemicIndexItem item = localItems.get(i);
                            final String itemName = item.getName().toString().toLowerCase();

                            // First match against the whole, non-splitted value
                            if (itemName.startsWith(prefixString)) {
                                filteredItems.add(item);
                            } else {} /* This is option and taken from the source of ArrayAdapter
                                final String[] words = itemName.split(" ");
                                final int wordCount = words.length;

                                for (int k = 0; k < wordCount; k++) {
                                    if (words[k].startsWith(prefixString)) {
                                        newItems.add(item);
                                        break;
                                    }
                                }
                            } */
                        }

                        // Set and return
                        results.values = filteredItems;
                        results.count = filteredItems.size();
                    }//end synchronized
                }

                return results;
            }

            @SuppressWarnings("unchecked")
            @Override
            protected void publishResults(CharSequence prefix, FilterResults results) {
                //noinspection unchecked
                synchronized(mLock) {
                    final ArrayList<GlycaemicIndexItem> localItems = (ArrayList<GlycaemicIndexItem>) results.values;
                    notifyDataSetChanged();
                    clear();
                    //Add the items back in
                    for (Iterator iterator = localItems.iterator(); iterator
                            .hasNext();) {
                        GlycaemicIndexItem gi = (GlycaemicIndexItem) iterator.next();
                        add(gi);
                    }
                }//end synchronized
            }
        }
    }
}

Leave a Comment