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):
As we want to display our rows in custom style, we also have a layout xml file for the row itself:
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
}
}
}
}