54

I've the Custom CustomerAdapter

public class CustomerAdapter extends ArrayAdapter<Customer> {
    private final String MY_DEBUG_TAG = "CustomerAdapter";
    private ArrayList<Customer> items;
    private int viewResourceId;

    public CustomerAdapter(Context context, int viewResourceId, ArrayList<Customer> items) {
        super(context, viewResourceId, items);
        this.items = items;
        this.viewResourceId = viewResourceId;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        if (v == null) {
            LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = vi.inflate(viewResourceId, null);
        }
        Customer customer = items.get(position);
        if (customer != null) {
            TextView customerNameLabel = (TextView) v.findViewById(R.id.customerNameLabel);
            if (customerNameLabel != null) {
                customerNameLabel.setText(String.valueOf(customer.getName()));
            }
        }
        return v;
    }
}

and customer_auto layout

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/customerNameLabel"
    android:layout_width="fill_parent" android:layout_height="fill_parent"
    android:padding="10dp" android:textSize="16sp" 
    android:textColor="#000">
</TextView>

and on my public void onCreate

AutoCompleteTextView customerAutoComplete = (AutoCompleteTextView) findViewById(R.id.autocomplete_customer);
CustomerAdapter customerAdapter = new CustomerAdapter(this, R.layout.customer_auto, customerList);
customerAutoComplete.setAdapter(customerAdapter);

and Customer.java

public class Customer implements Parcelable {

    private int id;
    private String name = "";

    public Customer() {
        // TODO Auto-generated constructor stub
    }

    /**
     * This will be used only by the MyCreator
     * 
     * @param source
     */
    public Customer(Parcel source) {
        /*
         * Reconstruct from the Parcel
         */
        id = source.readInt();
        name = source.readString();
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
    }

    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {

        @Override
        public Customer createFromParcel(Parcel source) {
            return new Customer(source);
        }

        @Override
        public Customer[] newArray(int size) {
            return new Customer[size];
            // TODO Auto-generated method stub
        }

    };

    @Override
    public String toString() {
        return this.name;
    }

}

But the auto suggest box does not filter correctly. for eg; if i type an in the test box customers starting with br are showing up!

peetsnack
  • 91
  • 10
Mithun Sreedharan
  • 49,883
  • 70
  • 181
  • 236
  • I think you need to check the list you are passing to CustomerAdapter. It might be having wrong data. From where you are generating that list? – kosa Jan 09 '12 at 05:28
  • No, `customerList` is having correct data, when I used the same in a `Spinner` it showed correctly – Mithun Sreedharan Jan 09 '12 at 05:33
  • Did you print them out before displaying in suggestion box? right before setText? that may give some clue. As well as I would suggest uninstall and re-install the app. – kosa Jan 09 '12 at 05:36

9 Answers9

127

I have to over-ride the getFilter() method of the Adapter

Here is the code which worked for me, thanks to sacoskun

public class CustomerAdapter extends ArrayAdapter<Customer> {
    private final String MY_DEBUG_TAG = "CustomerAdapter";
    private ArrayList<Customer> items;
    private ArrayList<Customer> itemsAll;
    private ArrayList<Customer> suggestions;
    private int viewResourceId;

    public CustomerAdapter(Context context, int viewResourceId, ArrayList<Customer> items) {
        super(context, viewResourceId, items);
        this.items = items;
        this.itemsAll = (ArrayList<Customer>) items.clone();
        this.suggestions = new ArrayList<Customer>();
        this.viewResourceId = viewResourceId;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        if (v == null) {
            LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = vi.inflate(viewResourceId, null);
        }
        Customer customer = items.get(position);
        if (customer != null) {
            TextView customerNameLabel = (TextView) v.findViewById(R.id.customerNameLabel);
            if (customerNameLabel != null) {
//              Log.i(MY_DEBUG_TAG, "getView Customer Name:"+customer.getName());
                customerNameLabel.setText(customer.getName());
            }
        }
        return v;
    }

    @Override
    public Filter getFilter() {
        return nameFilter;
    }

    Filter nameFilter = new Filter() {
        @Override
        public String convertResultToString(Object resultValue) {
            String str = ((Customer)(resultValue)).getName(); 
            return str;
        }
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            if(constraint != null) {
                suggestions.clear();
                for (Customer customer : itemsAll) {
                    if(customer.getName().toLowerCase().startsWith(constraint.toString().toLowerCase())){
                        suggestions.add(customer);
                    }
                }
                FilterResults filterResults = new FilterResults();
                filterResults.values = suggestions;
                filterResults.count = suggestions.size();
                return filterResults;
            } else {
                return new FilterResults();
            }
        }
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            ArrayList<Customer> filteredList = (ArrayList<Customer>) results.values;
            if(results != null && results.count > 0) {
                clear();
                for (Customer c : filteredList) {
                    add(c);
                }
                notifyDataSetChanged();
            }
        }
    };

}
Hoa Vu
  • 2,865
  • 4
  • 25
  • 33
Mithun Sreedharan
  • 49,883
  • 70
  • 181
  • 236
  • Thank you soo much for pointing me to the filter. IMHO it is a huge design error that it is so much work to override the lable there should be a simple getLable() method which should be used for comperation in the Filter. – rekire Oct 08 '13 at 08:16
  • Yes indeed its modifies the original list. need to check out – Mohammed Azharuddin Shaikh Dec 12 '13 at 06:53
  • 3
    Where is the original list being modified? I think I am running into that issue and cannot figure out where it is happening. – Mike Richards Aug 07 '14 at 12:35
  • @Mithun are you ever going to fix your answer here to incorporate the feedback from the other "answers" listed here? – Carl Anderson Nov 04 '14 at 22:09
  • Here is another best solution guys! http://stackoverflow.com/questions/19820736/error-using-notifydatasetchanged-in-android-array-adapter – nAkhmedov Nov 28 '14 at 05:32
  • Great! And you're free to call a webservice in `performFiltering()`, because it runs on a worker thread! – Aron Lorincz Jul 17 '15 at 10:12
  • your filter/publish result not work correctly, because when you try remove letter not update correctly ! –  Nov 12 '15 at 22:45
  • I receive same update problem with you and I fixed modidy orginal list issue. My solution is below. I hope it will help for you :) – Abdulkadir NURKALEM Jan 25 '16 at 06:36
  • original list is modified one entry left. how to fix this. – ralphgabb Oct 18 '16 at 09:09
  • I am having same problem can any one look into my [question](https://stackoverflow.com/questions/47034523/autocompletetextview-with-custom-adapter-filtering-not-working) ? – Moeez Oct 31 '17 at 12:18
  • You can't modify a Collection while iterating over it using an `Iterator` – Alireza Noorali Dec 09 '18 at 10:57
58

This is my solution. I feel like it's a bit cleaner (doesn't use 3 separate, confusing ArrayLists) than the accepted one, and has more options. It should work even if the user types backspace, because it doesn't remove the original entries from mCustomers (unlike the accepted answer):

public class CustomerAdapter extends ArrayAdapter<Customer> {
    private LayoutInflater layoutInflater;
    List<Customer> mCustomers;

    private Filter mFilter = new Filter() {
        @Override
        public String convertResultToString(Object resultValue) {
            return ((Customer)resultValue).getName();
        }

        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            FilterResults results = new FilterResults();

            if (constraint != null) {
                ArrayList<Customer> suggestions = new ArrayList<Customer>();
                for (Customer customer : mCustomers) {
                    // Note: change the "contains" to "startsWith" if you only want starting matches
                    if (customer.getName().toLowerCase().contains(constraint.toString().toLowerCase())) {
                        suggestions.add(customer);
                    }
                }

                results.values = suggestions;
                results.count = suggestions.size();
            }

            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            clear();
            if (results != null && results.count > 0) {
                // we have filtered results
                addAll((ArrayList<Customer>) results.values);
            } else {
                // no filter, add entire original list back in
                addAll(mCustomers);
            }
            notifyDataSetChanged();
        }
    };

    public CustomerAdapter(Context context, int textViewResourceId, List<Customer> customers) {
        super(context, textViewResourceId, customers);
        // copy all the customers into a master list
        mCustomers = new ArrayList<Customer>(customers.size());
        mCustomers.addAll(customers);
        layoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;

        if (view == null) {
            view = layoutInflater.inflate(R.layout.customerNameLabel, null);
        }

        Customer customer = getItem(position);

        TextView name = (TextView) view.findViewById(R.id.customerNameLabel);
        name.setText(customer.getName());

        return view;
    }

    @Override
    public Filter getFilter() {
        return mFilter;
    }
}
Carl Anderson
  • 3,446
  • 1
  • 25
  • 45
12

Instead of overriding getFilter() method in adapter, simply we can override the toString() of the userDefined object (Customer). In toString() just return the field based on what you need to filter. It worked for me.

In my example I'm filtering based on names:

public class Customer{
    private int id;
    private String name;

    @Override
    public String toString() {
        return this.name;
    }
}
Hexfire
  • 5,945
  • 8
  • 32
  • 42
kavinraj M
  • 149
  • 2
  • 5
3

In the above code publisHResults() method gives the concurrent modification exception.... we have to modify the code as:

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
    ArrayList<Customer> filteredList = (ArrayList<Customer>) results.values;
    ArrayList<Customer> customerList=new ArrayList<Customer>();
    if (results != null && results.count > 0) {
        clear();
        for (Customer c : filteredList) {
            customerList.add(c);
        }
        Iterator<Customer> customerIterator=getResult.iterator();
        while (customerIterator.hasNext()) {
            Customer customerIterator=customerIterator.next();
            add(customerIterator);
        }
        notifyDataSetChanged();
    }
}
biegleux
  • 13,179
  • 11
  • 45
  • 52
2

Maybe this is too late, you dont need to override all of these functions , the only function to override is :

  public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    if (v == null) {
        LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        v = vi.inflate(viewResourceId, null);
    }
    Customer customer = getItem(position);
    if (customer != null) {
        TextView customerNameLabel = (TextView) v.findViewById(R.id.customerNameLabel);
        if (customerNameLabel != null) {
            customerNameLabel.setText(String.valueOf(customer.getName()));
        }
    }
    return v;
}

consider I change :

 Customer customer = items.get(position);
 Customer customer = getItem(position);

pay attention, you should not declare new ListItems,

 private ArrayList<Customer> items;

because ArrayAdapter works with its own mObjects, and filter this list not your items list, So you should use getItem function to access items. then there is no reason to write your ArrayFilter.

Mehrdad Faraji
  • 3,744
  • 3
  • 25
  • 38
  • This is the correct answer, the results are incorrect since the items are filtered on the base class list and not on the custom member list. Once the `items` member is removed and `getItem(pos)` is used, the results will be correct as if the original `ArrayAdapter` was used. – SagiLow Jan 27 '17 at 17:03
1

I don't know where you retrieving the getResult. I think the solution in this case for don't have the concurrent modification is:

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
    ArrayList<Customer> filteredList = (ArrayList<Customer>) results.values;
    ArrayList<Customer> customerList=new ArrayList<Customer>();
    if (results != null && results.count > 0) {
        clear();

try{
            for (Customer c : filteredList) {
                customerList.add(c);
            }
}catch(Exception e){
Log.e("PEEEETAAAAAAAA", "AutoCompletaError: "+e.getMessage()+"  "+e.getCause()+" "+e.getLocalizedMessage());
            }

        Iterator<Customer> customerIterator=customerList.iterator();
        while (customerIterator.hasNext()) {
            Customer customerIterator=customerIterator.next();
            add(customerIterator);
        }
        notifyDataSetChanged();
    }
}
1

If you get ConcurrentModificationException exception.

Replace ArrayList with the thread safe CopyOnWriteArrayList.

Here you can find detatils answer

Agilanbu
  • 2,747
  • 2
  • 28
  • 33
Zoltán Buzás
  • 866
  • 1
  • 8
  • 9
1

I hope that this post will help people with implementation of a similar custom functionality in the future. I based this on my version of adapter used for displaying tag suggestions in my microblogging app:

public class TagSuggestionsAdapter extends ArrayAdapter<String> implements Filterable

Extending ArrayAdapter to have less boilerplate code. Implementing Filterable to change filter behavior later.

    private List<String> allTags;
    private List<String> tagSuggestions;      
    private Context context;

public TagSuggestionsAdapter(List<String> initialTagSuggestions, List<String> allTags,
                             Context context) {
    super(context, R.layout.item_tag_suggestion, initialTagSuggestions);
    this.tagSuggestions = initialTagSuggestions;
    this.allTags = allTags;   
    this.context = context;
}

Basically in constructor you need to pass a list that will be displayed initially - it'll later become a list with filtered results (this is also a reference to a list that will be taken into consideration when calling notifyDataSetChanged()) and obviously a list on which you can base your filtering (allTags in my case). I'm also passing Context for layout inflation in getView().

   @NonNull
    @Override
    public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) {

        ViewHolder viewHolder;    

        if (convertView == null) {
            convertView = LayoutInflater.from(context)
                    .inflate(R.layout.item_tag_suggestion, parent, false);
            viewHolder = new ViewHolder(convertView);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        viewHolder.tagSuggestionTextView.setText(tagSuggestions.get(position));

        return convertView;
    }

    static class ViewHolder {

        @BindView(R.id.tag_suggestion_text_view)
        TextView tagSuggestionTextView;

        ViewHolder(View itemView) {
            ButterKnife.bind(this, itemView);
        }
    }

Above you can see a simple view holder pattern with a little help from Butterknife to inflate a custom row layout.

 @NonNull
    @Override
    public Filter getFilter() {
        return new Filter() {

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                if (constraint != null) {
                    List<String> filteredTags = filterTagSuggestions(constraint.toString(), allTags);
                    FilterResults filterResults = new FilterResults();
                    filterResults.values = filteredTags;
                    filterResults.count = filteredTags.size();
                    return filterResults;
                } else {
                    return new FilterResults();
                }
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                tagSuggestions.clear();
                if (results != null && results.count > 0) {
                    List<?> filteredTags = (List<?>) results.values;
                    for (Object filteredTag : filteredTags) {
                        if (filteredTag instanceof String) {
                            tagSuggestions.add((String) filteredTag);
                        }
                    }
                }
                notifyDataSetChanged();
            }
        };
    }

This is the least boilerplate code I could write. Your only concern is method filterTagSuggestions that should return a filtered list of tags based on input from user (CharSequence constraint). Hope that summarized and organized necessary info a little bit.

matdziu
  • 13
  • 6
0

I have non-update and modify orginal list issues from above answer. I fixed this problem with this codes.

public class AdapterAutoCompleteTextView extends ArrayAdapter<ItemWord> {

    private int LayoutID;
    private int TextViewID;

    private LayoutInflater Inflater;

    private List<ItemWord> ObjectsList;

    public AdapterAutoCompleteTextView(Context ActivityContext, int ResourceID, int TextViewResourceID, List<ItemWord> WordList) {
        super(ActivityContext, ResourceID, TextViewResourceID, new ArrayList<ItemWord>());

        LayoutID = ResourceID;
        TextViewID = TextViewResourceID;

        ObjectsList = WordList;

        Inflater = LayoutInflater.from(ActivityContext);
    }

    @Override
    public View getView(int Position, View ConvertView, ViewGroup Parent) {
        ItemWord Word = getItem(Position);

        if(ConvertView == null) {
            ConvertView = Inflater.inflate(LayoutID, null);

            ResultHolder Holder = new ResultHolder();

            Holder.ResultLabel= (TextView) ConvertView.findViewById(TextViewID);

            ConvertView.setTag(Holder);
        }

        ResultHolder Holder = (ResultHolder) ConvertView.getTag();

        Holder.ResultLabel.setText(Word.getSpelling());

        return ConvertView;
    }

    @Override
    public Filter getFilter() {
        return CustomFilter;
    }

    private Filter CustomFilter = new Filter() {
        @Override
        public CharSequence convertResultToString(Object ResultValue) {
            return ((ItemWord) ResultValue).getSpelling();
        }

        @Override
        protected FilterResults performFiltering(CharSequence Constraint) {
            FilterResults ResultsFilter = new FilterResults();

            ArrayList<ItemWord> OriginalValues = new ArrayList<ItemWord>(ObjectsList);

            if(Constraint == null || Constraint.length() == 0){
                ResultsFilter.values = OriginalValues;
                ResultsFilter.count = OriginalValues.size();
            } else {
                String PrefixString = Constraint.toString().toLowerCase();

                final ArrayList<ItemWord> NewValues = new ArrayList<ItemWord>();

                for(ItemWord Word : OriginalValues){
                    String ValueText = Word.getSpelling().toLowerCase();

                    if(ValueText.startsWith(PrefixString))
                        NewValues.add(Word);
                }

                ResultsFilter.values = NewValues;
                ResultsFilter.count = NewValues.size();
            }

            return ResultsFilter;
        }

        @Override
        protected void publishResults(CharSequence Constraint, FilterResults Results) {
            clear();

            if(Results.count > 0)
                addAll(((ArrayList<ItemWord>) Results.values));
            else
                notifyDataSetInvalidated();
        }
    };

    private static class ResultHolder {
        TextView ResultLabel;
    }

}

This is most important line for non-update and modify orginal list issue:

super(ActivityContext, ResourceID, TextViewResourceID, new ArrayList<ItemWord>());

Particularly those

super(ActivityContext, ResourceID, TextViewResourceID, new ArrayList());

I hope this solution will be help you :)