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!
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:
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.
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.
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.
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.