Search code examples
androidsearchview

Searchview for partial word search


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
            }
        });
    }

}

Solution

  • 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();
                }
            }
    
        }
    
    }