Android AutocompleteTextView using ArrayAdapter and Filter

a.b.d picture a.b.d · Apr 7, 2012 · Viewed 7.9k times · Source

Backgroud information

The app I am currently writing deals with places. The main method to create a place is to enter an address. Providing a convenient way to do it is very important for the UX.

Current solution

After investigating the problem a while I came with the conclusion that the best way to do it is a text area with autocompletion based on the current location and user input (like in the Google Map application). Since this feature is unfortunately not provided by the Google Map API I have to implement it by myself.

Current implementation

To get address suggestions I use Geocoder. These suggestions are provided to an AutoCompleteTextView by a custom ArrayAdaptor through a custom Filter.

The ArrayAdapter (Some code has been removed)

public class AddressAutoCompleteAdapter extends ArrayAdapter<Address> {

@Inject private LayoutInflater layoutInflater;

private ArrayList<Address> suggested;
private ArrayList<Address> lastSuggested;
private String lastInput = null;
private AddressLoader addrLoader;

public AddressAutoCompleteAdapter(Activity activity, 
        AddressLoaderFactory addrLoaderFact,
        int maxSuggestionsCount) {
    super(activity,R.layout.autocomplete_address_item);
    suggested = new ArrayList<Address>();
    lastSuggested = new ArrayList<Address>();
    addrLoader = addrLoaderFact.create(10);
}

...

@Override
public Filter getFilter() {
    Filter myFilter = new Filter() {

        ...

        @SuppressWarnings("unchecked")
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            Log.d("MyPlaces","performFiltring call");
            FilterResults filterResults = new FilterResults();
            boolean debug = false;
            if(constraint != null) {
                // Load address
                try {
                    suggested = addrLoader.load(constraint.toString()); 
                }
                catch(IOException e) {
                    e.printStackTrace();
                    Log.d("MyPlaces","No data conection avilable");
                    /* ToDo : put something useful here */
                }
                // If the address loader returns some results
                if (suggested.size() > 0) {
                    filterResults.values = suggested;
                    filterResults.count = suggested.size();
                // If there are no result with the given input we check last input and result.
                // If the new input is more accurate than the previous, display result for the
                // previous one.
                } else if (constraint.toString().contains(lastInput)) {
                    debug = true;
                    Log.d("MyPlaces","Keep last suggestion : " + lastSuggested.get(0).toString());
                    filterResults.values = lastSuggested;
                    filterResults.count = lastSuggested.size();
                } else {
                    filterResults.values = new ArrayList<Address>();
                    filterResults.count = 0;
                }

                if ( filterResults.count > 0) {
                    lastSuggested = (ArrayList<Address>) filterResults.values;
                }
                lastInput = constraint.toString();
            }
            if (debug) {
                Log.d("MyPlaces","filter result count :" + filterResults.count);
            }
            return filterResults;
        }

        @Override
        protected void publishResults(CharSequence contraint, FilterResults results) {
            AddressAutoCompleteAdapter.this.notifyDataSetChanged();
        }
    };
    return myFilter;
}

...

The AddressLoader (some code has been removed)

public class GeocoderAddressLoader implements AddressLoader {

private Application app;
private int maxSuggestionsCount;
private LocationManager locationManager;

@Inject
public GeocoderAddressLoader(
        Application app,
        LocationManager locationManager,
        @Assisted int maxSuggestionsCount){
    this.maxSuggestionsCount = maxSuggestionsCount;
    this.app = app;
    this.locationManager = locationManager;
}

/**
 * Take a string representing an address or a place and return a list of corresponding
 * addresses
 * @param addrString A String representing an address or place
 * @return List of Address corresponding to the given address description
 * @throws IOException If Geocoder API cannot be called
 */
public ArrayList<Address> load(String addrString) throws IOException {
    //Try to filter with the current location
    Location current = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
    Geocoder geocoder = new Geocoder(app,Locale.getDefault());
    ArrayList<Address> local = new ArrayList<Address>();
    if (current != null) {
        local = (ArrayList<Address>) geocoder.getFromLocationName(
                addrString,
                maxSuggestionsCount,
                current.getLatitude()-0.1,
                current.getLongitude()-0.1,
                current.getLatitude()+0.1,
                current.getLongitude()+0.1);
    }
    if (local.size() < maxSuggestionsCount) {
        ArrayList<Address> global = (ArrayList<Address>) geocoder.getFromLocationName(
                addrString, maxSuggestionsCount - local.size());
        for (Address globalAddr : global) {
            if (!containsAddress(local,globalAddr))
                local.add(globalAddr);
        }
    }
    return local;
}

...

Problems

This implementation does work, but isn't perfect.

The AddressLoader have a strange behavior with some input.
For example if the user want to enter this address

10 rue Guy Ropartz 54600 Villers-les-Nancy  

When he have enter :

10 rue Guy

there are some suggestions but not the desired address.
If the he continue entering text when this input is reached:

10 rue Guy Ro

suggestions disappear. The desired address apear finally when the input is

10 rue Guy Ropart

I think this is disturbing for the user. it's strange that there is no suggestion for "10 rue Guy Ro" while there are suggestions for "10 rue Guy" and "10 rue Guy Ropart"...

A possible workaround

I imagine a possible workaround for this issue and try to implement it. The idea is to keep previous suggestion if after typing a longer address there are no address suggested by AdressLoader. In our exemple keep "10 rue Guy" suggestions when the input is "10 rue Guy Ro".

Unfortunately my code doesn't work. When I am in this situation the good portion of code is reached :

else if (constraint.toString().contains(lastInput)) {
                debug = true;
                Log.d("MyPlaces","Keep last suggestion : " + lastSuggested.get(0).toString());
                filterResults.values = lastSuggested;
                filterResults.count = lastSuggested.size();
            }

and the FilterResult returned by performFiltering is filled with the good suggestions but it doesn't appear on the view. Maybe I miss something in publishResults...

Questions

  • Geocoder has sometime a strange behavior, maybe there is a better approach to get address suggestions ?
  • Can you suggest me a better workaround that the one I described ?
  • What is the problem in the implementation of my workaround ?

Update
For compatibility concerns, I implemented a new AddressLoader based on the Google Geocode Http API (because the default Android geocoder service is not available on all devices). It reproduce the exact same strange behavior.

Answer

Harish picture Harish · Oct 9, 2012

The Geocoder api is not designed to give suggestions on partial strings like "10 rue Guy Ro" . You can try this in Google maps, enter "10 rue Guy Ro" and leave a space. You will not get any suggestions(Google maps is using Geocoder) but when you remove the space you will get suggestions on the partial string (Google Maps is using private API). You can use the Geocoder api the same way Google maps use, fetch the suggestion only after the user input space in the string he is typing.