Good day,
I have a following problem. In my Android application I have a list of entries in XML that contains bus stop name and ID. Those are put in a HashMap as IDs are unique, while stop names are not. The user interface of activity contains an AutoCompleteTextView and Spinner.
My objective is to populate auto-complete view with stop names and then pass the ID of selected stop to the other class that will display bus lines on that stop in spinner (via remote API).
So what the user will do is start typing stop name (e.g. Awesome Stop) and he will see two entries in auto-complete suggestions. Depending on which one he will select spinner will show different results.
My problem is that I can't figure out how to couple AutoCompleteTextView and HashMap. Auto-complete works well with ArrayAdapter<String> populated via ArrayList<String> but it's not terribly helpful because I can only get stop name back, and it's not very helpful since I actually need ID.
Many thanks for any tip.
OK, after a fair bit of time I figured it out, thanks to the tip from joaquin. It was indeed done by implementing custom adapter. And the HashMap was not very helpful in original goal. Below is the code, if someone stumbles upon a similar challenge.
Activity:
// Members
private long currentStopId;
protected Map<String,String> lineMap;
protected ArrayList<Stop> stopMap;
protected String previousUrl = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_line);
// Get the list of stops from resourse XML
getStopInformation();
// Enable auto-complete
stopInput = (AutoCompleteTextView) findViewById(R.id.inputStopName);
final HashMapAdapter adapter = new HashMapAdapter(this,R.layout.stop_list,stopMap);
stopInput.setAdapter(adapter);
// Add listener for auto-complete selection
stopInput.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position, long rowId) {
String selection = (String)parent.getItemAtPosition(position);
// There we get the ID.
currentStopId = parent.getItemIdAtPosition(position);
}
});
}
Adapter implementation:
public class StopAdapter extends BaseAdapter implements Filterable {
private ArrayList<Stop> inputStopList;
private ArrayList<Stop> inputStopListClone;
private LayoutInflater lInflater;
/** Constructor */
public StopAdapter(Context context, int textViewResourceId, ArrayList<Stop> input) {
lInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inputStopList = input;
inputStopListClone = inputStopList;
}
#Override
public int getCount() {
return inputStopList.size();
}
#Override
public Object getItem(int i) {
Stop value = inputStopList.get(i);
return value.getStopName();
}
#Override
public long getItemId(int i) {
Stop stop = inputStopList.get(i);
long value = Long.parseLong(stop.getStopCode());
return value;
}
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
View myView = view;
// R.layout.stop_list created in res/layout
if(myView == null)
myView = lInflater.inflate(R.layout.stop_list,viewGroup, false);
((TextView) myView.findViewById(R.id.textView)).setText(getItem(i).toString());
return myView;
}
/** Required by AutoCompleteTextView to filter suggestions. */
#Override
public Filter getFilter() {
Filter filter = new Filter() {
#Override
protected FilterResults performFiltering(CharSequence charSequence) {
FilterResults filterResults = new FilterResults();
if(charSequence == null || charSequence.length() == 0) {
ArrayList<Stop> originalValues = new ArrayList<Stop>(inputStopList);
filterResults.count = originalValues.size();
filterResults.values = originalValues;
}
else {
ArrayList<Stop> newValues = new ArrayList<Stop>();
// Note the clone - original list gets stripped
for(Stop stop : inputStopListClone)
{
String lowercase = stop.getStopName().toLowerCase();
if(lowercase.startsWith(charSequence.toString().toLowerCase()))
newValues.add(stop);
}
filterResults.count = newValues.size();
filterResults.values = newValues;
}
return filterResults;
}
#Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
if(filterResults != null && filterResults.count > 0) {
inputStopList = (ArrayList<Stop>)filterResults.values;
notifyDataSetChanged();
}
else notifyDataSetInvalidated();
}
};
return filter;
}
}
You are using AutoCompleteTextView instead of MultiAutoCompleteTextView.
MultiAutoCompleteTextView is exactly what you need because it works exactly equal as AutoCompleteTextView with the difference that it can show more than one suggestion (if they exist) and lets the user choose only one of them.
Reference Image: http://i.stack.imgur.com/9wMAv.png
For a nice example check out Android Developers: http://developer.android.com/reference/android/widget/MultiAutoCompleteTextView.html
Related
I followed some youtube tutorial about SQLite and RecyclerView (https://www.youtube.com/watch?v=VQKq9RHMS_0&ab_channel=Stevdza-San) I completed all the things and customize it as I need to my app and everything work fine.
Now all I want to do is add Search Button at the top (Action Bar) that i can search items from my RecyclerView.
So i was try to figure out how to implement it, but I struggle with the filter and all those things. Every tutorials that I saw about this work with List (E) while my CustomeAdpter geting ArrayList and I can't undrstand how to make it work with the ArrayList.
So if some one can help me with that and give me some little guidance I will be grateful
EDIT
I added the getFilter() function in my adapter, but when I try to search something nothing happened in RecyclerView.
I am adding my code: MainActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
activity = MainActivity.this;
noClubsImage = findViewById(R.id.noClubsImage);
noClubsText = findViewById(R.id.noClubsText);
recyclerView = findViewById(R.id.recyclerView);
add_button = findViewById(R.id.add_button);
add_button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, AddActivity.class);
activity.startActivityForResult(intent,1);
}
});
myDB = new MyDatabaseHelper(MainActivity.this);
club_id = new ArrayList<>();
club_name = new ArrayList<>();
join_date = new ArrayList<>();
expire_date = new ArrayList<>();
storeDataInArrays();
customAdapter = new CustomAdapter(MainActivity.this, this, club_id, club_name, join_date, expire_date);
recyclerView.setAdapter(customAdapter);
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
}
.
.
.
EDIT***
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
MenuItem item = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) item.getActionView();
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
customAdapter.getFilter().filter(query);
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
customAdapter.getFilter().filter(newText);
return true;
}
});
return super.onCreateOptionsMenu(menu);
}
}
And this is my Adapter:
I added Try & catch function in onBindViewHolder becuase it's crash if not and get this error: "java.lang.IndexOutOfBoundsException: Index: 1, Size: 1"
After adding it, I can search but something very weird, upload some pictures:
When I start the app
Start searching "Yasha" -> it change the two rows
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.MyViewHolder> implements Filterable{
private Context context;
private Activity activity;
private ArrayList club_id, club_name, join_date,expire_date,list,originalList;
int position;
Animation translate_anim;
CustomAdapter(Activity activity, Context context, ArrayList club_id, ArrayList club_name, ArrayList join_date, ArrayList expire_date){
this.activity = activity;
this.context = context;
this.club_id = club_id;
this.club_name = club_name;
this.join_date = join_date;
this.expire_date = expire_date;
this.list = club_name;
this.originalList = club_name;
}
#NonNull
#Override
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.my_row,parent,false);
return new MyViewHolder(view);
}
public Filter getFilter(){
return new Filter() {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
ArrayList filteredResults = null;
if (constraint.length() == 0) {
filteredResults = club_name;
} else {
filteredResults = getFilteredResults(constraint.toString().toLowerCase());
}
FilterResults results = new FilterResults();
results.values = filteredResults;
Log.d("Test",filteredResults.toString());
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
club_name = (ArrayList<String>) results.values;
notifyDataSetChanged();
}
};
}
protected ArrayList getFilteredResults(String constraint) {
ArrayList results = new ArrayList<>();
for (Object item : originalList) {
if (item.toString().toLowerCase().contains(constraint)) {
results.add(item.toString());
}
}
//Log.d("Test",results.toString());
return results;
}
#Override
public void onBindViewHolder(#NonNull MyViewHolder holder, final int position) {
try{
this.position = position;
holder.club_name_txt.setText(String.valueOf(club_name.get(position)));
holder.join_txt.setText(String.valueOf(join_date.get(position)));
holder.expire_txt.setText(String.valueOf(expire_date.get(position)));
holder.mainLayout.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(context,UpdateActivity.class);
intent.putExtra("club_id",String.valueOf(club_id.get(position)));
intent.putExtra("club_name",String.valueOf(club_name.get(position)));
intent.putExtra("join_date",String.valueOf(join_date.get(position)));
intent.putExtra("expire_date",String.valueOf(expire_date.get(position)));
activity.startActivityForResult(intent,1);
}
});
}catch(Exception ex) {
Log.e("TAG", "EXCEPTION CAUGHT WHILE EXECUTING DATABASE TRANSACTION");
ex.printStackTrace();
}
}
#Override
public int getItemCount() {
return club_id.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
LinearLayout mainLayout;
TextView club_id_txt, club_name_txt, join_txt,expire_txt;
public MyViewHolder(#NonNull View itemView) {
super(itemView);
club_name_txt = itemView.findViewById(R.id.club_name_txt);
join_txt = itemView.findViewById(R.id.jDate_txt);
expire_txt = itemView.findViewById(R.id.eDate_txt);
mainLayout = itemView.findViewById(R.id.mainLayout);
translate_anim = AnimationUtils.loadAnimation(context, R.anim.translate_anim);
mainLayout.setAnimation(translate_anim);
}
}}
you have customAdapter.getFilter(); line in both methods of OnQueryTextListener and it returns filter for you. thats all what you have requested, you've never passed a String to this filter. this line should look like this
customAdapter.getFilter().filter(query); // or newText, depend on OnQueryTextListener method
edit due to comments:
remove this try{}catch() and fix your exception. this method is reliable and only your mistake can lead to crash.
you are filtering and publishing only one of arrays (club_name = (ArrayList<String>) results.values;), but still you are informing adapter that list have number of items equal to club_id.size(); in getItemCount. thats why you are getting IndexOutOfBoundsException - club_id is untouched and have multiple items, even after filtering when club_name have only few or one item inside. you should filter all four arrays, in fact this should be one array with custom object carrying all four params
some tutorial for fixing:
keep your data in some additional arrays which will stay untouched and keep all data even after filtering. for start make "original" and "working" arrays
CustomAdapter(Activity activity, Context context, ArrayList club_id, ArrayList club_name, ArrayList join_date, ArrayList expire_date){
this.activity = activity;
this.context = context;
this.club_id_org = club_id;
this.club_name_org = club_name;
this.join_date_org = join_date;
this.expire_date_org = expire_date;
this.club_id = new ArrayList<>();
this.club_id.addAll(this.club_id_org);
this.club_name = new ArrayList<>();
this.club_name.addAll(this.club_name_org);
this.join_date = new ArrayList<>();
this.join_date.addAll(this.join_date_org);
this.expire_date = new ArrayList<>();
this.expire_date.addAll(this.expire_date_org);
}
note how much duplicated code we have in here, thats why this should be one ArrayList with some custom object... but nvm, lets stay (for now) with four arrays
dont use this construction: this.list = club_name; - this makes list and club_name are same arrays (objects), when you remove some item from one then it will be removed from second (in fact same) array, and we need here two separated arrays (x4)
now all arrays with _org suffix are carrying whole lists, but adapter should work on "working" arrays, these without _org, like you have currently in code
now filtering: in performFiltering you should create, again, four temporary arrays and fill them with filtered items. for filtering iterate through _org arrays, these with all items
#Override
protected FilterResults performFiltering(CharSequence constraint) {
ArrayList club_id_temp = new Arraylist<>(),
club_name_temp = new Arraylist<>(),
join_date_temp = new Arraylist<>(),
expire_date_temp = new Arraylist<>();
if (constraint.length() == 0) {
club_id_temp.addAll(club_id_org);
club_name_temp.addAll(club_name_org);
join_date_temp.addAll(join_date_org);
expire_date_temp.addAll(expire_date_org);
} else {
for(int i=0; i<club_name_org.size(); i++){
if(club_name_org.get(i).toLowerCase().contains(constraint.toLowerCase())){
club_id_temp.add(club_id_org.get(i));
club_name_temp.add(club_name_org.get(i));
join_date_temp.add(join_date_org.get(i));
expire_date_temp.add(expire_date_org.get(i));
}
}
}
FilterResults results = new FilterResults();
results.values = ...
and now we have a small problem... results.values can carry only one object, e.g. one ArrayList and you have four... lets make come class carrying all four for your purposes, declare such e.g. on the bottom of adapter (just before last bracket closing whole adapter })
public static class TempArrays{
public ArrayList club_id, club_name, join_date, expire_date;
}
and pack all four filtered arrays into:
FilterResults results = new FilterResults();
TempArrays ta = new TempArrays();
ta.club_id = club_id_temp;
ta.club_name = club_name_temp;
ta.join_date = join_date_temp;
ta.expire_date = expire_date_temp;
results.values = ta;
return results;
}
and unpack this construction in publishResults:
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
TempArrays ta = (TempArrays) results.values;
club_id = ta.club_id;
club_name = ta.club_name;
join_date = ta.join_date;
expire_date = ta.expire_date;
notifyDataSetChanged();
}
again: four arrays isn't proper way, look how much duplicated lines we have here... + some small workaround for passing all four after filtering instead of one... in fact there should be some ClubModel class, similar to this
public static class ClubModel{
public String club_id, club_name, join_date, expire_date;
}
and then you may work on one array with ClubModel items, instead of four arrays. check out POJO definition
I have an listview with filter. When I input some words in an edittext that I used as a filter for example "david", it works well, items in the list are filtered and it will show all item that contains "david". But when I delete some words, for example "dav", the list is still filtered, but it filtered from the last filtered by "david".
Let's say I had 40 items, filtered by "david", it becomes 24 items. Then I filtered it again with "dav", it filtered from the "24 items" one, not the "40 items" one.
Here is my custom adapter:
public class WRegistrantListAdapter extends ArrayAdapter<Registrant> {
private Context mContext;
private int mResource;
private List<Registrant> mOriginalList;
private List<Registrant> mFilteredList;
public WRegistrantListAdapter(Context context, int resource, ArrayList<Registrant> oobjects, int workshopItemId) {
super(context, resource, oobjects);
mContext = context;
mResource = resource;
mFilteredList = oobjects;
}
#NonNull
#Override
public View getView(int position, #Nullable View convertView, #NonNull ViewGroup parent) {
//contains code for displaying item.
}
#NonNull
#Override
public Filter getFilter() {
return new Filter() {
#Override
protected FilterResults performFiltering(CharSequence charSequence) {
FilterResults result = new FilterResults();
String constraint = charSequence.toString().toLowerCase();
if (mOriginalList == null) {
mOriginalList = mFilteredList;
Toast.makeText(mContext, String.valueOf(mOriginalList.size()), Toast.LENGTH_SHORT).show();
}
if (constraint == null || constraint.isEmpty() || constraint.equals("")) {
result.values = mOriginalList;
result.count = mOriginalList.size();
} else {
List<Registrant> list = new ArrayList<>();
int max = mOriginalList.size();
for (int cont = 0; cont < max; cont++) {
Registrant item = mOriginalList.get(cont);
boolean contains =
item.getRegistrantName().toLowerCase().contains(constraint) ||
item.getRegistrantNumber().toLowerCase().contains(constraint);
if (contains) {
list.add(mOriginalList.get(cont));
}
}
result.values = list;
result.count = list.size();
}
return result;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
clear();
addAll((ArrayList<Registrant>) results.values);
notifyDataSetChanged();
}
};
}
}
Which part in the filtering is wrong? Any help would be much appreciated. Hope my explanation is not confusing because English is not my mother language.
You need two different lists for filtering, so try change mOriginalList = mFilteredList; to mOriginalList = new ArrayList<>(mFilteredList); may solve the issue.
Explanations:
mOriginalList = mFilteredList; is same list with two different names. It is helpful in modular program, just like mFilteredList = oobjects; in your adapter constructor.
mOriginalList = new ArrayList<>(mFilteredList); is to make a shallow copy of mFilteredList and store it as mOriginalList, so the lists are different.
Shallow and Deep Copy:
Example: If your custom class, Registrant, contains a public field (List, Map or custom object etc., that requires new for creation) named sample. Under shallow copy, mOriginalList = new ArrayList<>(mFilteredList);, mOriginalList.get(i) is a copy of mFilteredList.get(i) and they are 2 different Registrant objects. But mOriginalList.get(i).sample and mFilteredList.get(i).sample is the same object.
If you need mOriginalList.get(i).sample and mFilteredList.get(i).sample to be different objects, then it is called deep copy. There is no ready method to make deep copy, you have to make your own method according to your custom class. But up to now, I never have a case that needs deep copy.
Hope that helps!
You should keep two separate lists in your adapter such as,
private List<Registrant> mOriginalList = new ArrayList();
private List<Registrant> mFilteredList = new ArrayList();
public WRegistrantListAdapter(Context context, int resource, ArrayList<Registrant> oobjects, int workshopItemId) {
super(context, resource, oobjects);
mContext = context;
mResource = resource;
mFilteredList.addAll(oobjects);
mOriginalList.addAll(oobjects);
}
Initially, both of them should have the same value and you will use filteredList for showing your data. Later in the filter, you should publish your data like
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
filteredList.clear();
filteredList.addAll((ArrayList<Registrant>) results.values);
notifyDataSetChanged();
}
A complete example is can be found in Filter ListView with arrayadapter
I have an Android Application, and there's a part where the user can use an AutoCompleteTextView to select names. The user can select a name and then details (such as address) would appear in the next activity. What I do is that when the user selects a name, I get that name's position from the ArrayList, and go to my Address ArrayList, get the address at the same position, and then I have my data.
However, some of the names tend to repeat, for example, I have a Name ArrayList as such:
[A, B, B, C]
and an Address ArrayList as such:
[Address A, Address B, Address C, Address D]
when the user clicks the 2nd B in the AutoCompleteTextView, the position returned will be that of the first B, NOT the second B, and therefore, it will give the Address B instead of Address C.
I've looked around and I've tried to make my custom ArrayAdapter for my AutoCompleteTextView to display my custom class with the Name and the Address. However, I did not have much luck with that.
The next thing I looked into was to using a HashMap<String, String> as shown in thisStackOverflow Question. It is very similar to what is used for a Custom ListView, only that it has a getFilter function. I have tried to adapt that code as such:
public class actvMapFilter extends
ArrayAdapter<Map<String, String>> implements Filterable {
private Context mContext;
private List<Map<String, String>> mClientList;
private List<Map<String, String>> mMutableList;
public actvMapFilter(Context context,
List<Map<String, String>> objects) {
super(context, android.R.layout.simple_list_item_1, objects);
mContext = context;
mClientList = objects;
// TODO Auto-generated constructor stub
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(android.R.layout.simple_dropdown_item_1line, null);
}
TextView nameView = (TextView) v.findViewById(android.R.id.text1);
if(position < mClientList.size()){
Map<String, String> clientMap = mClientList.get(position);
nameView.setText(clientMap.keySet().toArray()[position].toString());
}
return v;
}
#Override
public Filter getFilter() {
return new Filter() {
#Override
protected void publishResults(CharSequence constraint,
FilterResults results) {
if (results.count > 0) {
mClientList = (ArrayList<Map<String, String>>) results.values;
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
ArrayList<Map<String, String>> result = new ArrayList<Map<String, String>>();
HashMap<String,String> myMap = new HashMap<String,String>();
if(constraint != null && mClientList!=null){
int length = mClientList.size();
int i = 0;
Log.e("","constraint = " + constraint.toString().toUpperCase());
Log.e("","constraint and list not null.");
Log.e("","length of original list = " + length);
while(i < length){
String key = mClientList.get(i).keySet().toArray()[0].toString();
String value = mClientList.get(i).values().toArray()[0].toString();
if(key.contains(constraint.toString().toUpperCase())){
myMap.put(key, value);
result.add(myMap);
}
i++;
}
Log.e("","filter results = " + myMap);
Log.e("","filter results count = " + myMap.size());
filterResults.values = result;
filterResults.count = result.size();
}
FilterResults r = new FilterResults();
r.values = result;
r.count = result.size();
return r;
}
};
}
}
It is very similar to what was marked as the answer. However, what I do in my getFilter function is that when I have a constraint given, I check if the key part contains the constraints, I add those.
This works well, I am told by my LogCat that if I type a certain character, my resulting HashMap would contain the entries with the constrains only.
However, what happens is that when I type in a 2nd succeeding character, the filter does not seem to work any more. I am given a blank filtered result HashMap and the dropdown menu reflects the blank result because I don't have any suggestive text any more.
How can I implement a Filterable ArrayAdapter properly?
It seems that this should be easy.
I have an Android app with a list using an ArrayAdapter. It works. I replace the ArrayAdapter with a custom sub-class. It works. I add a inner class that is a sub-class of Filter and mark the ArrayAdaptor sub-class as implementing Fiterable. And it works fine, except that it does not filter.
What is the magic word I have to say here?
None of the methods in the Filter sub-class are being invoked.
public class XYZListAdapter extends ArrayAdapter<XYZListFragment.XYZItem> implements Filterable {
private List<XYZListFragment.XYZItem> sourceObjects;
private Context sourceContext;
private XYZFilter xyzFilter;
public XYZListAdapter(Context context, int resource, int textViewResourceId, List<XYZListFragment.XYZItem> objects) {
super(context, resource, textViewResourceId, objects);
sourceObjects = new ArrayList<>(objects);
sourceContext = context;
}
public View getView(int position, View convertView, ViewGroup parent) {
Log.i("XYZAdapter", "getView");
View v = convertView;
if (v == null) {
v = ((LayoutInflater)sourceContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.fragment_xyz_list_item, parent, false);
}
XYZListFragment.XYZItem mItem = sourceObjects.get(position);
if (mItem != null) {
TextView t = (TextView)v.findViewById(R.id.xyz_list_item_name_view);
if (t != null) {
t.setText(mItem.name());
}
}
return v;
}
#Override
public Filter getFilter() {
Log.i("XYZAdapter", "getFilter");
if (xyzFilter == null)
xyzFilter = new XYZFilter();
return xyzFilter;
}
private class XYZFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
Log.i("XYZFilter", "perform");
FilterResults results = new FilterResults();
if (constraint == null || constraint.length() == 0) {
results.values = sourceObjects;
results.count = sourceObjects.size();
}
else {
List<XYZListFragment.XYZItem> nextXYZList = new ArrayList<>();
for (XYZListFragment.XYZItem p : sourceObjects) {
if (p.name().toUpperCase().startsWith(constraint.toString().toUpperCase()))
nextXYZList.add(p);
}
results.values = nextXYZList;
results.count = nextXYZList.size();
}
return results;
}
#Override
#SuppressWarnings("unchecked")
protected void publishResults(CharSequence constraint, FilterResults results) {
Log.i("XYZFilter", "publish");
if (results.count == 0)
notifyDataSetInvalidated();
else {
sourceObjects = (List<XYZListFragment.XYZItem>) results.values;
notifyDataSetChanged();
}
}
}
}
It doesn't work because you shouldn't subclass the ArrayAdapter if you want to customize it's filtering logic. You would need to implement an adapter from scratch using the BaseAdapter.
Basically, the ArrayAdapter internally tracks its own list of objects. However you are only updating your list of objects in the subclass. You need to update the internal list as well. Otherwise the adapter is not aware of the change.
There are other issues. For instance, the performFiltering() method executes on a background thread. That means you need to synchronize any changes done to the list. The ArrayAdapter already does this internally...and you have no way of accessing the sync lock object it uses...which makes it impossible for you to safely sync.
Also, your filtering solution doesn't seem to provide a solution for restoring the list back to its original state after the filtering is cleared out.
You can read more about the problems with ArrayAdapter filtering here. General rule of thumb. When using the ArrayAdapter don't create your own list to track the data and don't customize the filtering. Optionally, you can use something like the AbsArrayAdapter which is just like an ArrayAdapter but lets you define the filtering logic for you.
I am new to Android development and I ran into a problem which I find difficult to solve. I am trying to figure out how to use an AutoCompleteTextView widget properly. I want to create a AutoCompleteTextView, using XML data from a web service. I managed to get it to work, but I am defenitely not pleased with the output.
I would like to put a HashMap with id => name pairs into the AutoCompleteTextView and get the id of the clicked item. When I click on the autocomplete filtered set output, I want to populate a list underneath the autocompletion box, which I also managed to get to work.
Done so far:
autocomplete works well for simple ArrayList, all data filtered correct
onItemClick event fires properly after click
parent.getItemAtPosition(position) returns correct String representation of the clicked item
The event onItemClick(AdapterView parent, View v, int position, long id) does not behave as I would like. How can I figure out the unfiltered array position of the clicked item? The position of the filtered one is the one I am not interested in.
Further questions:
How to handle HashMaps or Collections in AutoCompleteTextView
How to get the right itemId in the onItemClick event
I did very extensive research on this issue, but did not find any valuable information which would answer my questions.
How to handle HashMaps or Collections in AutoCompleteTextView
You can set your own custom adapter. In your adapter it's up to you where you get your data to a given position.
How to get the right itemId in the onItemClick event
In your custom adapter, you define a filter, and that filter sets the suggested items. You have two different list, one with the original values, and another one with the filtered items. I mean something like this.
private class AutoCompleteItemAdapter extends ArrayAdapter<YourItemClass> implements Filterable {
private NameFilter mFilter;
List<YourItemClass> suggestions;
List<YourItemClass> mOriginalValues;
public AutoCompleteItemAdapter(Context context, int resource, List<YourItemClass> suggestions) {
super(context, resource, suggestions);
this.suggestions = suggestions;
this.mOriginalValues = suggestions;
}
public void updateData(List<YourItemClass> suggestions) {
mLock.lock();
try{
this.suggestions = suggestions;
this.mOriginalValues = suggestions;
finally{
mLock.unlock();
}
}
#Override
public int getCount() {
mLock.lock();
try {
return suggestions.size();
} finally {
mLock.unlock();
}
}
#Override
public YourItemClass getItem(int position) {
return mOriginalValues.get(position);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// draw your item here...
}
#Override
public Filter getFilter() {
if (mFilter == null) {
mFilter = new NameFilter();
}
return mFilter;
}
private class NameFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
mLock.lock();
try {
mOriginalValues = new ArrayList<YourItemClass>(suggestions);
} finally {
mLock.unlock();
}
}
if (prefix == null || prefix.length() == 0) {
mLock.lock();
try {
ArrayList<YourItemClass> list = new ArrayList<YourItemClass>(mOriginalValues);
results.values = list;
results.count = list.size();
} finally {
mLock.unlock();
}
} else {
String prefixString = prefix.toString().toLowerCase();
final List<YourItemClass> values = mOriginalValues;
final int count = values.size();
final ArrayList<YourItemClass> newValues = new ArrayList<YourItemClass>(count);
// FILTERING
//
// add your hits to the newValues collection
//
//
results.values = newValues;
results.count = newValues.size();
}
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mLock.lock();
try {
if (results == null || results.values == null) return;
suggestions = new ArrayList<YourItemClass>();
suggestions = (List<YourItemClass>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
} finally {
mLock.unlock();
}
}
}
}
Now this might raise some concurrency issue, as we the reference, the adapter might ask for the size of the list, and a bigger value goes out, which might cause problem in the getView function. (fi.: Attempting to draw 5 element with the underlying data has only 4, because we did another filtering) This the way we used our AutoCompleteTextView, and so far it works great, no problem. I just mentioned that I'm still concerned, and I have this vague feeling that there is a better solution for this.
In your onClick listener, you use the returned value(from the filtered list) as the key in your map, and get the associated value. You can think your list you use an index for the HashMap. After that, you can use your Map to draw your item, or to get your own data.