This is frustrating, I have spent a week trying to find out how to increase the performance of a custom autocompleteTextView in Android and come out with nothing. I have even implemented a Trie to keep some indexes of the list items in order to recover them when filtering, but in vain (I guess I am using it improperly or the problem is somewhere else). I have implemented a FilterSearchTaskFragment in order to keep a reference to the last started task and to stop it when the user types fastly many characters in the autocompleteTextView.
Inside the performFiltering() method and the publishResults() I have put some controls to recover the state of the actual running task (with a call to the isCancelled() method) in order to make it finish as soon as possible but nothing, or better I have increased the performance a little bit when I test the app in the ADT simulator, but on a real device (my tablet) it seems like things remained the same... Why? What am I messing?
I have googled in order to find some useful answers but I didn't find anyone with the same problem. I have even asked around here on SO and someone told me to use a Loader, someone told me to use a Handler, but when the situation came to facts, no one replied to me and I have no idea on how I can use them in my situation. I am using a custom List of objects (I am filtering some municipalities data which I previously recover from a MySQL DB). Here is all the code I wrote:
First the FilterSearchTaskFragment:
public class FilterSearchTaskFragment extends Fragment {
private TaskCallbacks filterSearchTaskCallbacks;
private static int taskNum;
public interface TaskCallbacks {
void onPreExecute();
void onPostExecute();
void performFilterSearch();
}
private AsyncFilterSearchAbstractTask filterSearchTask;
private boolean running;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (activity instanceof TaskCallbacks) {
filterSearchTaskCallbacks = (TaskCallbacks) activity;
}
else {
throw new ClassCastException(activity.toString() + " must implement the FilterSearchTaskFragment.TaskCallbacks interface!");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
// Start the background filter search task
public void start() {
filterSearchTask = new AsyncFilterSearchAbstractTask();
filterSearchTask.execute();
taskNum++;
running = true;
}
public int getTaskNum() {
return taskNum;
}
// Cancel the execution of the background filter search task
public void cancel() {
filterSearchTask.cancel(true);
filterSearchTask = null;
running = false;
}
public boolean isRunning() {
return this.running;
}
public boolean isCancelled() {
if (filterSearchTask.isCancelled()) {
return true;
}
return false;
}
// Background filter search
private class AsyncFilterSearchAbstractTask extends AsyncTask<Void, Void, Void> {
@Override
protected void onPreExecute() {
filterSearchTaskCallbacks.onPreExecute();
running = true;
}
@Override
protected Void doInBackground(Void... nothings) {
filterSearchTaskCallbacks.performFilterSearch();
return null;
}
@Override
protected void onCancelled() {
Log.i("TASK CANCELLED", "THE TASK NUMBER " + getTaskNum() + " HAS BEEN CANCELLED");
}
@Override
protected void onPostExecute(Void nothing) {
filterSearchTaskCallbacks.onPostExecute();
running = false;
}
}
}
It has an interface (TaskCallbacks) which my Activity implements:
public void onPreExecute() {
}
public void performFilterSearch() {
if (!filterSearchTask.isCancelled()) {
adapter.setFilterSearchTask(filterSearchTask);
adapter.setConstraint(constraint);
adapter.setStartChangeOffset(startChangeOffset);
adapter.setBeforeChars(beforeChars);
adapter.setCountCharsThatReplacedBefore(countCharsThatReplacedBefore);
adapter.setNotifyOnChange(false);
if (!filterSearchTask.isCancelled()) {
adapter.getFilter().filter(constraint);
}
}
}
public void onPostExecute() {
}
The filter work is delegated to a Custom Adapter, which contains the Filter implementation, here is the class:
public class MunicipalitySearchAdapter extends ArrayAdapter<Municipality> {
private ArrayList<Municipality> municipalities;
private ArrayList<Municipality> allMunicipalities;
private ArrayList<Municipality> suggestedMunicipalities;
FilterSearchTaskFragment filterSearchTask;
private List<Trieable> triableList;
private Trie municipalityTrie;
private int viewResourceId;
@SuppressWarnings("unchecked")
public MunicipalitySearchAdapter(Context context, int viewResourceId, ArrayList<Municipality> municipalities) {
super(context, viewResourceId, municipalities);
this.municipalities = municipalities;
this.allMunicipalities = (ArrayList<Municipality>) this.municipalities.clone();
this.suggestedMunicipalities = new ArrayList<Municipality>();
this.viewResourceId = viewResourceId;
this.triableList = new ArrayList<Trieable>();
for (Municipality mun : allMunicipalities) {
triableList.add(mun);
}
municipalityTrie = new Trie(triableList, Locale.ITALY);
}
@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(this.viewResourceId, null);
}
Municipality municipality = municipalities.get(position);
if (municipality != null) {
TextView munNameTxtView = (TextView) v.findViewById(R.id.name);
TextView proSignTxtView = (TextView) v.findViewById(R.id.sign);
TextView regNameTxtView = (TextView) v.findViewById(R.id.regionName);
if (munNameTxtView != null) {
munNameTxtView.setText(municipality.getName());
}
if (proSignTxtView != null) {
proSignTxtView.setText(municipality.getProvinceSign());
}
if (regNameTxtView != null) {
regNameTxtView.setText(municipality.getRegionName());
}
}
return v;
}
@Override
public Filter getFilter() {
return municipalityFilter;
}
Filter municipalityFilter = new Filter() {
@Override
public String convertResultToString(Object resultValue) {
String str = ((Municipality) (resultValue)).getName();
return str;
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterRes = new FilterResults();
if (constraint == null || constraint.length() == 0) {
filterRes.values = allMunicipalities;
filterRes.count = allMunicipalities.size();
}
else {
String constraintString = constraint.toString().trim().toLowerCase(Locale.ITALY);
suggestedMunicipalities.clear();
List<Integer> wordsIndexesList = municipalityTrie.getWordsIndexes(municipalityTrie.getRootVertex(), constraintString);
if (wordsIndexesList != null) {
for (int i = 0; i<wordsIndexesList.size() && !filterSearchTask.isCancelled(); i++) {
int index = wordsIndexesList.get(i);
suggestedMunicipalities.add(allMunicipalities.get(index));
}
}
List<Integer> prefixesIndexesList = municipalityTrie.getPrefixesIndexes(municipalityTrie.getRootVertex(), constraintString);
if (prefixesIndexesList != null) {
for (int i = 0; i<prefixesIndexesList.size() && !filterSearchTask.isCancelled(); i++) {
int index = prefixesIndexesList.get(i);
suggestedMunicipalities.add(allMunicipalities.get(index));
}
}
filterRes.values = suggestedMunicipalities;
filterRes.count = suggestedMunicipalities.size();
}
return filterRes;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
@SuppressWarnings("unchecked")
ArrayList<Municipality> filteredMunicipalities = (ArrayList<Municipality>) results.values;
ArrayList<Municipality> supportMunicipalitiesList = new ArrayList<Municipality>();
notifyDataSetChanged();
clear();
if (!filterSearchTask.isCancelled()) {
for (int i = 0; i < filteredMunicipalities.size() && !filterSearchTask.isCancelled(); i++) {
Municipality mun = filteredMunicipalities.get(i);
supportMunicipalitiesList.add(mun);
}
Iterator<Municipality> municipalityIterator = supportMunicipalitiesList.iterator();
while (municipalityIterator.hasNext() && !filterSearchTask.isCancelled()) {
Municipality municipality = municipalityIterator.next();
add(municipality);
}
}
notifyDataSetInvalidated();
}
}
};
}
The Trie class used in the filtering operation is this:
public class Trie {
private Vertex rootVertex;
public Trie(List<Trieable> objectList, Locale locale) {
rootVertex = new Vertex();
for (int i = 0; i<objectList.size(); i++) {
String word = objectList.get(i).toString().toLowerCase(locale);
addWord(rootVertex, word, i);
}
}
public Vertex getRootVertex() {
return rootVertex;
}
public void addWord(Vertex vertex, String word, int index) {
if (word.isEmpty()) {
vertex.addIndexToWordsIndexList(index);
vertex.increaseWordsNumber();
}
else {
vertex.addIndexToPrefixesIndexList(index);
vertex.increasePrefixesNumber();
Character fChar = word.charAt(0);
HashMap<Character, Vertex> vertexSons = vertex.getVertexSons();
if (!vertexSons.containsKey(fChar)) {
vertex.addVertexSon(fChar);
}
word = (word.length() == 1) ? "" : word.substring(1);
addWord(vertexSons.get(fChar), word, index);
}
}
public List<Integer> getWordsIndexes(Vertex vertex, String word) {
if (word.isEmpty()) {
return vertex.getWordsIndexList();
}
else {
Character fChar = word.charAt(0);
if (!(vertex.getVertexSons().containsKey(fChar))) {
return null;
}
else {
word = (word.length() == 1) ? "" : word.substring(1);
return getWordsIndexes(vertex.getVertexSons().get(fChar), word);
}
}
}
public List<Integer> getPrefixesIndexes(Vertex vertex, String prefix) {
if (prefix.isEmpty()) {
return vertex.getPrefixesIndexList();
}
else {
Character fChar = prefix.charAt(0);
if (!(vertex.getVertexSons().containsKey(fChar))) {
return null;
}
else {
prefix = (prefix.length() == 1) ? "" : prefix.substring(1);
return getPrefixesIndexes(vertex.getVertexSons().get(fChar), prefix);
}
}
}
}
Every node is represented by a Vertex of the Trie, this is the class:
public class Vertex {
private HashMap<Character, Vertex> vertexSons;
private List<Integer> wordsIndexList;
private List<Integer> prefixesIndexList;
private int wordsNumber;
private int prefixesNumber;
public Vertex() {
vertexSons = new HashMap<Character, Vertex>();
wordsIndexList = new ArrayList<Integer>();
prefixesIndexList = new ArrayList<Integer>();
wordsNumber = 0;
prefixesNumber = 0;
}
public boolean hasWords() {
if (wordsNumber > 0) {
return true;
}
return false;
}
public boolean hasPrefixes() {
if (prefixesNumber > 0) {
return true;
}
return false;
}
public void addVertexSon(Character character) {
vertexSons.put(character, new Vertex());
}
public void addIndexToWordsIndexList(int index) {
wordsIndexList.add(index);
}
public void addIndexToPrefixesIndexList(int index) {
prefixesIndexList.add(index);
}
public HashMap<Character, Vertex> getVertexSons() {
return vertexSons;
}
public List<Integer> getWordsIndexList() {
return wordsIndexList;
}
public List<Integer> getPrefixesIndexList() {
return prefixesIndexList;
}
public int getWordsNumber() {
return wordsNumber;
}
public int getPrefixesNumber() {
return prefixesNumber;
}
public void increaseWordsNumber() {
wordsNumber++;
}
public void increasePrefixesNumber() {
prefixesNumber++;
}
}
And finally here is where I instantiate the adapter (in my Activity) and add a listener to the AutocompleteTextView:
adapter = new MunicipalitySearchAdapter(getApplicationContext(), R.layout.municipality_list_item, ListaComuni);
municipalityEditTxt.setAdapter(adapter);
municipalityEditTxt.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
constraint = s;
startChangeOffset = start;
beforeChars = before;
countCharsThatReplacedBefore = count;
if (filterSearchTask.isRunning()) {
filterSearchTask.cancel();
}
filterSearchTask.start();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
The municipalityEditText is the AutocompleteTextView reference. So please if someone of you here on SO have faced the same problem, could you tell me how did you make it work smoothly??? Please answer me cause I got stuck!
Thanks!