AutoCompleteTextView - prevent dropdown from closing while calling notifyDataSetChanged - android

I'm using my own Adapter and my own Filter to populate AutoCompleteTextView. Everything works fine except that every time when I'm calling notifyDataSetChanged dropdown with is closed and then reopens again with new suggestions (which is kind a annoying).
What I'm trying to achieve - is the same behaviour as Google. When you typing word, dropdown just populated with new values (without reopening). Is there any workarounds?
PS
Adapter populated in Filter#publishResults:
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if(results == null) {
return;
}
mAdapter.clear(); //notifyDataSetChanged is NOT called here
List<?> content = (List<?>) results.values;
final int size = content.size();
for(int i=0; i<size; i++) {
mAdapter.add((City) content.get(i));
}
mAdapter.notifyDataSetChanged();
}

Related

How to show data by object's type in RecyclerAdapter?

I have a list of items with types [a,b,c,d] indicates status of item. When I click on ButtonA I want to show items with types [a,b] in recyclerview, and click on ButtonB show items with types [c,d]. My current solutions is using two list and two adapter, I wonder if there's better approach, thank you.
Its really Easy! I guess in your object model u have Boolean field. You can use Filterable interface.
Just implement it in activity/fragment or viewModel to filter list that u r passing to adapter according to that boolean value or any other criteria inside respective clicklisteners of ur buttons. Its quite simple and intuitive.
Or just share ur code i can do it for you. I really want points ))
The job of an RecyclerAdapter is to show the data you're passing to it.
Unfortunately you don't provide any code, so I assume that the buttons are outside your RecyclerView.
Place a method inside your RecyclerAdapater which you can call from outside. The notifyDataSetChanged() re-runs onBindViewHolder() with your new provided data.
public void updateList(List<YourObjectType> yourObjects) {
this.yourObjects = yourObjects;
notifyDataSetChanged();
}
I solve my problem by implement Filterable interface in adapter. onCreate in activity, after initial adapter to recyclerview, I added this:
adapter.getFilter().filter("u");
Here is my adapter code:
#Override
public Filter getFilter() {
return new Filter() {
#Override
protected FilterResults performFiltering(CharSequence charSequence) {
if (charSequence.equals("u")) {
List<Transaction> filteredList = new ArrayList<>();
for (Transaction trans : allTrans) {
if (trans.getTr_stt().equalsIgnoreCase("0") ||
trans.getTr_stt().equalsIgnoreCase("2") ||
trans.getTr_stt().equalsIgnoreCase("5")) {
filteredList.add(trans);
}
}
filteredTrans = filteredList;
} else {
List<Transaction> filteredList = new ArrayList<>();
for (Transaction trans : allTrans) {
if (trans.getTr_stt().equalsIgnoreCase("1") ||
trans.getTr_stt().equalsIgnoreCase("3") ||
trans.getTr_stt().equalsIgnoreCase("4")) {
filteredList.add(trans);
}
}
filteredTrans = filteredList;
}
FilterResults filterResults = new FilterResults();
filterResults.values = filteredTrans;
return filterResults;
}
#Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
filteredTrans = (ArrayList<Transaction>) filterResults.values;
notifyDataSetChanged();
}
};
}

RecyclerView item layout not updated

I am implementing Edit text with RecyclerView where RecylecerView should update its item and its View when user enter any text in the edit text. It is a very common requirement and i have found tons of solution for this. But i m facing a strange issue that after filtering recylcerView is showing wrong item in the list.
To Illustrate. lets suppose RecylcerView contains items as a,b,c,d and e.
if i search 'c. list shows only one item 'a'. however when i click on this item it is 'c' only. it means only layout not getting updated however values do get updated.
Here is the my implementation.
public class CustomFilter extends Filter{
private CustomFilter(SListRecyclerViewAdapter mAdapter) {
super();
}
#Override
protected FilterResults performFiltering(CharSequence constraint) {
sData.clear();
final FilterResults results = new FilterResults();
if (constraint.length() == 0) {
sData.addAll(filteredsData);
}
else {
final String filterPattern = constraint.toString().toLowerCase().trim();
for (final S surg: filteredsData) {
if (S.getSUserName().toLowerCase(Locale.getDefault()).contains(filterPattern)) {
sData.add(surg);
}
}
}
results.values = sData;
results.count = sData.size();
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
notifyDataSetChanged();
}
Please see the below example,you have to update the data with result.Please try
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mFilteredData = (ArrayList<String>) results.values;
notifyDataSetChanged();
}
I dont why it is working this way. But i have found a work around.
recyclerView.swapAdapter(sListRecyclerViewAdapter,false);
It is basically swapping the adapter with new one but recycling earlier views. Its not very much optimal but at least workable.

Custom ListView ArrayAdapter not displaying filtered data

I am displaying some data in ListView using my custom ArrayAdapter class. I have a spinner in Action Bar and based on it's item selection I want to filter the data and repopulate it in the list view.
Currently when my activity loads the data shows up correctly, but when I select any item from the spinner, the ListView gets empty and does not show any data, although the Actionbar still appears.
Could you please tell me what I am doing wrong?
This is my custom ArrayAdapter Class :
public class MyAdapter extends ArrayAdapter implements Filterable {
private ArrayList < ArrayList < String >> arrayList;
private LayoutInflater theInflater;
public MyAdapter(Context context, ArrayList arrayList) {
super(context, R.layout.row_layout, arrayList);
this.arrayList = arrayList;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
theInflater = LayoutInflater.from(getContext());
View theView = theInflater.inflate(R.layout.row_layout, parent, false);
ArrayList < String > values = (ArrayList < String > ) getItem(position);
TextView a= (TextView) theView.findViewById(R.id.a);
TextView b= (TextView) theView.findViewById(R.id.b);
TextView c= (TextView) theView.findViewById(R.id.c);
a.setText(values.get(0));
b.setText(values.get(1));
c.setText(values.get(2));
return theView;
}
public Filter geFilter() {
Filter filter = new Filter() {#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
ArrayList al = new ArrayList();
al.add("ValueA1");
al.add("ValueA2");
al.add("ValueA3");
ArrayList bl = new ArrayList();
bl.add("ValueB1");
bl.add("ValueB2");
bl.add("ValueB3");
ArrayList < ArrayList < String >> array = new ArrayList < > ();
array.add(al);
array.add(bl);
results.count = array.size();
results.values = array;
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
arrayList = (ArrayList) results.values;
notifyDataSetChanged();
}
};
return filter;
}
}
In Spinner's item selected listener, I have :
theAdapter.getFilter().filter("Incoming");
Unfortunately, there's a lot you are doing wrong with the adapter. Such as not implementing the ViewHolder paradigm. Note, this is not why you are seeing issues. However it does solve performance issues your solution will create.
The filtering itself is not working because ArrayAdapter tracks it's own internal list of data. The ArrayAdapter makes no guarantees that the list you are tracking externally is the same one. This is especially true during a filter operation. You can read more about the dangers of tracking an external list like you are here.
Your Filter is very problematic.
It re-assigns your external list of data to a new list, which means your arrayList is no longer the same one the internal ArrayAdapter is tracking. This will cause issues later on when you mutate the adapter.
As it stands, your implementation doesn't seem to actually filter anything. Usually you'll also have a second ArrayList in order to track the original data before the filtering starts. Otherwise there's no way to restore the data that was filtered out.
performFiltering() takes place on a background thread. While you don't seem to access arrayList during this method (which would be the only way to actually filter the data), you need to ensure all list modifications are synchronized. ArrayAdapter itself already handles synchronization but there's no way to sync your external arrayList with it. Meaning you can't actually write a filter without risking concurrent modification errors.
In publishResults() when results.count == 0, you must notifyDataSetInvalidated(). If results.count > 0, you must notifyDataSetChanged().
ArrayAdapter already implements Filterable. It's redundant to add that to your custom adapter.
There are a few bugs with the ArrayAdapter own filterable implementation. Here's a piece which talks about the problem and how to work around it. The relevance is that it discusses how to create your own filterable logic to fix the problem. The short, you can't create your own custom filter with the ArrayAdapter. You need to create your own adapter from scratch using BaseAdapter instead.
Alternatively, you can use a third party library which provides a very easy means to implement your own custom filtering logic. I highly suggest taking a look at that guy instead. Additionaly, I highly recommend you Google around more on how to properly create a custom adapter. There's a lot to them and a lot you'll need to learn to properly implement one from scratch.
Do the following in publishResults() method:
Clear the list, add the resultant data again and call notifyDataSetInvalidated() again.
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
arrayList = (ArrayList) results.values;
notifyDataSetChanged();
clear();
for(int i = 0, l = arrayList.size(); i < l; i++) {
add(arrayList.get(i));
}
notifyDataSetInvalidated();
}

Android : AutoCompleteTextView listview scroll listener (for endless scrolling)

I'm using an AutoCompleteTextView to let the user of my android app search for content using a custom webservice : it's working as intended but, for now, I can't find a way to implement "Endless Scrolling" on the dropdown listview.
Right now, my AutoCompleteTextView adapter is a ArrayAdapter implementing the Filterable interface; whenever the user changes the text of the AutoCompleteTextView, the performFiltering() method of my Filter is triggered and I can make an HTTP request to my custom webservice to display appropriate content. But I would like to load more content as the user scroll the dropdown, a paging system, so I can avoid loading like hundred of results at once ... and I can't figure how to!
How can I get the ListView associated with the AutoCompleteTextView
to implement my own OnScrollListener / EndlessScrollListener ?
Is there any other way ?
Thanks guys :)
My Fragment code
AutoCompleteTextView search = (AutoCompleteTextView) view.findViewById(R.id.search);
SearchAdapter searchAdapter = new SearchAdapter(getActivity(), 0);
search.setAdapter(searchAdapter);
My Adapter code (edited)
class SearchAdapter extends ArrayAdapter<Parcelable> implements Filterable {
Integer numberPerPage = 10;
Boolean moreDataIsAvailable = false;
FragmentActivity activity;
public ArrayList<Parcelable> items = new ArrayList<Parcelable>();
public SearchAdapter(FragmentActivity a, int textViewResourceId) {
super(a, textViewResourceId);
activity = a;
}
#Override
public int getCount() {
return items.size();
}
#Override
public Parcelable getItem(int index) {
if(items.size() > index) {
return items.get(index);
} else {
return null;
}
}
#Override
public Filter getFilter() {
Filter filter = new Filter() {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null && constraint.toString().length() >= 3) {
autocomplete(constraint.toString(), items);
filterResults.count = items.size();
filterResults.values = items;
} else {
items.clear();
filterResults.count = items.size();
filterResults.values = items;
}
return filterResults;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
return filter;
}
static class ViewHolder {
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
View rowView = convertView;
if (rowView == null) {
rowView = LayoutInflater.from(activity).inflate(R.layout.search_row, parent, false);
holder = new ViewHolder();
rowView.setTag(holder);
} else {
holder = (ViewHolder) rowView.getTag();
}
// Setting my row data
return rowView;
}
private void autocomplete(String input, ArrayList<Parcelable> items) {
ArrayList<Parcelable> data = new ArrayList<Parcelable>();
try {
RequestHandler request = new RequestHandler();
JSONObject requestParameters = new JSONObject();
requestParameters.put("offset", 0);
requestParameters.put("keyword", input);
requestParameters.put("limit", numberPerPage);
ResponseDescription response = request.request(activity, requestParameters);
if(!response.error) {
JSONArray searchedItems = response.getJSONArray("items");
if(searchedItems.length() == numberPerPage) {
moreDataIsAvailable = true;
} else {
moreDataIsAvailable = false;
}
for(int i = 0 ; i < searchedItems.length(); i++) {
JSONObject searchedItem = searchedItems.getJSONObject(i);
MyObject object = new MyObject();
object.initWithJSONObject(searchedItem);
data.add(object);
}
}
} catch (Exception e) {
e.printStackTrace();
}
items.clear();
items.addAll(data);
}
}
Unfortunately there's no OnScrollListener interface for autocomplete, but I think you might be able to get around that.
Here's what I think the recipe is:
Add another argument parameter to your autocomplete method for the offset.
I noticed you have '0' hard-coded for the offset. You'll want to be able to call autocomplete with a value for that. Also you have items.clear() at the end of autocomplete and you're only going to want to do that when the offset is zero.
Make an AsyncTask for your autocomplete method.
Or something with a way that you can run autocomplete in the background besides the performFiltering method in the Filter. This async task will need access to your adapter so it can add its results to the items list and call notifyDataSetChanged() just like the filter.
Your adapter will need to keep a reference to this task so you can cancel it if user starts typing again.
Add some logic to getView() to execute the async task.
We don't have an OnScrollListener so we'll use getView() as a sort of proxy for that.
Set a constant threshold for starting the next request. It needs to be less than your numberPerPage, so let's say '5' as an example.
Now, when the ListView calls getView() with a position that is within the threshold from the end, execute the async task. For example, after the first filter operation, you have 10 items in your list. When ListView requests a view for item 5, start the async task with an offset equal to your list size - in this case, 10 (to get items 11-20).
I'm basing this on the assumption that ListView will only request an item view if & when the user has scrolled down to that item.
In performFiltering(), cancel any running async task.
I have done this type of "endless" scroll, with a ListView calling an AsyncTask from OnScrollListener to return search results page by page, and I think what made it work for me is that the request response contained a total size, so I was able to use that for getCount().
I have also used autocomplete to get remote results, but I didn't have to page those results. Matter of fact, I think I just ran the async task when the input changed and my filter methods were no-ops.
You have a different situation combining the autocomplete and the paging. You might get some stuttering behavior from scrolling the dropdown if the user reaches the end of the list before the async task can update it. So you may have to play around with the page size, the threshold and even the return value for getCount() to get an acceptable scrolling experience. But I think it's doable.

Dynamically updating an AutoCompleteTextView adapter

I want to periodically change the suggestions given by an AutoCompleteTextview by getting the list from a RESTful web service, and can't get it working smoothly. I set up a hard-coded list of suggestions to make sure it's working:
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.list_item, new String[] {"Hi", "Ho"});
speciesName.setAdapter(adapter);//my autocomplete tv
I have got a TextWatcher on the textview and when the text changes that launches a non-blocking call to get a new list of suggestions -- this part which gets a new list is working fine. Then I want to reset the adapter, like so:
public void setOptionsAndUpdate(String[] options) {
Log.d(TAG, "setting options");
//speciesName.setAdapter((ArrayAdapter<String>)null);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.list_item, options);
speciesName.setAdapter(adapter);
}
This method is called, but doesn't work -- the list of suggestions either disappears or the displayed suggestions remain unchanged despite the call to setAdapter.
Is this even the right approach? I looked at SimpleCursorAdapter but couldn't see how to register my web service as a content provider. (It's of the form http://www.blah.com/query?term=XX, where the XX is the input from my app, and the response is a JSON Array of strings.)
I didn't have any luck using adapter.notifyDataSetChanged() when dynamically adding and changing the data in the adapter. In my situation, I was hitting an external api asynchronously and getting a list of completion data periodically.
This code clears the adapter, and adds the new data as you'd expect. However, I had to call the getFilter().Filter method to force the data to show. Also, I had to explicitly filter based on the current text in the AutocompleteTextView because my api call was asynchronous.
adapter.clear();
for (Map<String, String> map : completions) {
adapter.add(map.get("name"));
}
//Force the adapter to filter itself, necessary to show new data.
//Filter based on the current text because api call is asynchronous.
adapter.getFilter().filter(autocompleteTextView.getText(), null);
This is how I update my AutoCompleteTextView:
String[] data = terms.toArray(new String[terms.size()]); // terms is a List<String>
ArrayAdapter<?> adapter = new ArrayAdapter<Object>(activity, android.R.layout.simple_dropdown_item_1line, data);
keywordField.setAdapter(adapter); // keywordField is a AutoCompleteTextView
if(terms.size() < 40) keywordField.setThreshold(1);
else keywordField.setThreshold(2);
Now of course, this is static and doesn't deal with an over-the-air suggestions but, I can also suggest you to notify adapter for the changes after you assign it to the AutoCompleteTextView:
adapter.notifyDataSetChanged();
Hope this helps.
-serkan
There was a pretty good tutorial on this topic using remote data in the Google Map API to populate a AutoCompleteTextView here.
If you need a cached version, I retrieved it from here.
The original tutorial has been deleted, but essentially you need to write an ArrayAdapter with a custom filter in a similar way to that shown below and assign it to your AutoCompleteTextView.
Note: You need to implement a method autocomplete() that does whatever operation is required to synchronously fetch and return the autocompletion items. As the filter is invoked in a background thread, this will not block the main UI thread.
private class PlacesAutoCompleteAdapter extends ArrayAdapter<String> implements Filterable {
private ArrayList<String> resultList;
public PlacesAutoCompleteAdapter(Context context, int textViewResourceId) {
super(context, textViewResourceId);
}
#Override
public int getCount() {
return resultList.size();
}
#Override
public String getItem(int index) {
return resultList.get(index);
}
#Override
public Filter getFilter() {
Filter filter = new Filter() {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null) {
// Retrieve the autocomplete results.
resultList = autocomplete(constraint.toString());
// Assign the data to the FilterResults
filterResults.values = resultList;
filterResults.count = resultList.size();
}
return filterResults;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
notifyDataSetChanged();
}
else {
notifyDataSetInvalidated();
}
}};
return filter;
}
}
Since i am not able to add a comment, i am giving a new answer
There is no need for clearing the adapter or calling adapter.getFilter().filter(...)...
To dynamically update an AutoCompleteTextView adapter, simply add the new item to the adapter and setAdapter again. For the example given in the original question, i tried the following and it works (the code below does not show the initial setting of the adapter, since multiple answers here cover that. This just shows updating the adapter dynamically). The adapter update can be alongside the code that updates the List associated with the ArrayAdapter.
adapter.add(String newSuggestion); // this goes inside a loop for adding multiple suggestions
speciesName.setAdapter(adapter) ; // speciesName is an AutoCompleteTextView as given in the original question.
The best solution I found for updating the adapter:
Editable text = autocomplete.getText();
autocomplete.setText(text);
autocomplete.setSelection(text.length());
How it works:
We set the text of autoCompleteTextView with its current text, so the adapter notifies that data is changed and updates the listViews's content.
But by this trick the cursor moves to the beginning of edittext. so we use autocomplete.setSelection(text.length()) for moving the cursor to the end.
Works like a charm!
Edit:
Also you must use clear(), add() and remove() methods directly on your ArrayAdapter instead of your ArrayList.

Categories

Resources