I want to implement a SearchView
in Android for partial word search.
I have made the following search mechanism, how do I get partial word search functionality?
Eg. If I search for "stackover..", stackoverflow appears but if I search for "tackover.." stackoverflow doesn't appear, I need it to search for partial matches in words.
package jagranerp.myapplication;
import java.util.ArrayList;
import java.util.HashMap;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
public class MainActivity extends Activity {
// List view
private ListView lv;
// Listview Adapter
ArrayAdapter<String> adapter;
// Search EditText
EditText inputSearch;
// ArrayList for Listview
ArrayList<HashMap<String, String>> productList;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Listview Data
String products[] = {"Dell Inspiron", "HTC One X", "HTC Wildfire S", "HTC Sense", "HTC Sensation XE",
"iPhone 4S", "Samsung Galaxy Note 800",
"Samsung Galaxy S3", "MacBook Air", "Mac Mini", "MacBook Pro"};
lv = (ListView) findViewById(R.id.list_view);
inputSearch = (EditText) findViewById(R.id.inputSearch);
// Adding items to listview
adapter = new ArrayAdapter<String>(this, R.layout.list_item, R.id.product_name, products);
lv.setAdapter(adapter);
/**
* Enabling Search Filter
* */
inputSearch.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
// When user changed the Text
MainActivity.this.adapter.getFilter().filter(cs);
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
int arg3) {
// TODO Auto-generated method stub
}
@Override
public void afterTextChanged(Editable arg0) {
// TODO Auto-generated method stub
}
});
}
}
I was interested in doing something similar to this requirement myself so I played around with some stuff. As a result this code isn't just a short snippet / quick fix.
I created my own CustomArrayAdapter
which extends BaseAdapter
and is mostly based on the source code for ArrayAdapter. I havent implemented methods that add, remove or modify items in the adapter's list but those methods can easily be copied / adapted from the source (NOTE those methods use synchronize
to make the adapter thread-safe - be sure to follow that model).
The key thing is to create your own Filter
which in ArrayAdapter
is an inner private class so it's not just a simple case of extending ArrayAdapter
directly.
The answer from chntgomez points in the right direction - the Filter
for ArrayAdapter
simply uses startsWith(...)
to match the constraint. It first tries it on the start of the complete string and then attempts to split the string (using space char as a delimiter) to check to see if multi-word strings startWith(...)
the constraint (prefix).
By changing the use of startsWith(...)
to contains(...)
you can achieve a 'match' on an individual char or sequence of characters. The code to split any multi-word strings can also be removed as it's not necessary.
The following CustomArrayAdapter
and its Filter
works with Activity
posted in the original question (obviously changing ArrayAdapter
to be CustomArrayAdapter
instead).
public class CustomArrayAdapter<T> extends BaseAdapter implements Filterable {
private List<T> mObjects;
private final Object mLock = new Object();
private ArrayList<T> mOriginalValues;
private Filter mFilter;
private int mResource;
private int mDropDownResource;
private int mFieldId = 0;
private boolean mNotifyOnChange = true;
private Context mContext;
private LayoutInflater mInflater;
public CustomArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects) {
init(context, resource, textViewResourceId, Arrays.asList(objects));
}
private void init(Context context, int resource, int textViewResourceId, List<T> objects) {
mContext = context;
mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mResource = mDropDownResource = resource;
mObjects = objects;
mFieldId = textViewResourceId;
}
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
mNotifyOnChange = true;
}
@Override
public Filter getFilter() {
if (mFilter == null) {
mFilter = new CustomArrayFilter();
}
return mFilter;
}
@Override
public int getCount() {
return mObjects.size();
}
@Override
public T getItem(int position) {
return mObjects.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return createViewFromResource(position, convertView, parent, mResource);
}
private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource) {
View view;
TextView text;
if (convertView == null) {
view = mInflater.inflate(resource, parent, false);
} else {
view = convertView;
}
try {
if (mFieldId == 0) {
// If no custom field is assigned, assume the whole resource is a TextView
text = (TextView) view;
} else {
// Otherwise, find the TextView field within the layout
text = (TextView) view.findViewById(mFieldId);
}
} catch (ClassCastException e) {
Log.e("CustomArrayAdapter", "You must supply a resource ID for a TextView");
throw new IllegalStateException(
"CustomArrayAdapter requires the resource ID to be a TextView", e);
}
T item = getItem(position);
if (item instanceof CharSequence) {
text.setText((CharSequence)item);
} else {
text.setText(item.toString());
}
return view;
}
private class CustomArrayFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence matchChars) {
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<T>(mObjects);
}
}
if (matchChars == null || matchChars.length() == 0) {
ArrayList<T> list;
synchronized (mLock) {
list = new ArrayList<T>(mOriginalValues);
}
results.values = list;
results.count = list.size();
} else {
String matchString = matchChars.toString().toLowerCase();
ArrayList<T> values;
synchronized (mLock) {
values = new ArrayList<T>(mOriginalValues);
}
final int count = values.size();
final ArrayList<T> newValues = new ArrayList<T>();
for (int i = 0; i < count; i++) {
final T value = values.get(i);
final String valueText = value.toString().toLowerCase();
if (valueText.contains(matchString)) {
newValues.add(value);
}
}
results.values = newValues;
results.count = newValues.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mObjects = (List<T>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
}