Приложение, которое я сейчас пишу, имеет дело с местами. Основным способом создания места является ввод адреса. Предоставление удобного способа сделать это очень важно для UX.
После изучения проблемы некоторое время я пришел к выводу, что лучший способ сделать это — текстовая область с автозаполнением на основе текущего местоположения и пользовательского ввода (как в приложении Google Map). Поскольку эта функция, к сожалению, не предоставляется API Карт Google, я должен реализовать ее самостоятельно.
Чтобы получить предложения адресов, я использую Geocoder. Эти предложения предоставляются AutoCompleteTextViewпользовательским ArrayAdaptorпосредством пользовательского Filter.
ArrayAdapter (часть кода была удалена)
public class AddressAutoCompleteAdapter extends ArrayAdapter {
@Inject private LayoutInflater layoutInflater;
private ArrayList suggested;
private ArrayList 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();
lastSuggested = new ArrayList();
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();
filterResults.count = 0;
}
if ( filterResults.count > 0) {
lastSuggested = (ArrayList) 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;
}
...
AddressLoader (часть кода была удалена)
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 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 local = new ArrayList();
if (current != null) {
local = (ArrayList) 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 global = (ArrayList) geocoder.getFromLocationName(
addrString, maxSuggestionsCount - local.size());
for (Address globalAddr : global) {
if (!containsAddress(local,globalAddr))
local.add(globalAddr);
}
}
return local;
}
...
Эта реализация работает, но не идеальна.
AddressLoader имеет странное поведение при вводе некоторых данных.
Например, если пользователь хочет ввести этот адрес
10 rue Guy Ropartz 54600 Villers-les-Nancy
, когда он введет:
10 rue Guy
, есть некоторые предложения, но нет желаемого адреса.
Если он продолжит ввод текста при достижении этого ввода: предложения
10 rue Guy Ro
исчезнут.Желаемый адрес появляется, наконец, когда ввод
10 rue Guy Ropart
. Я думаю, что это беспокоит пользователя. странно, что нет предложений для "10 rue Guy Ro", а есть предложения для "10 rue Guy"и "10 rue Guy Ropart". ..
Я представляю возможный обходной путь для этой проблемы и пытаюсь реализовать его. Идея состоит в том, чтобы сохранить предыдущее предложение, если после ввода более длинного адреса нет адресов, предложенных AdressLoader. В нашем примере сохраняйте предложения "10 rue Guy"при вводе "10 rue Guy Ro".
К сожалению, мой код не работает. Когда я нахожусь в этой ситуации, достигается хорошая часть кода:
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();
}
и FilterResult, возвращаемый PerformFiltering, заполнен хорошими предложениями, но не отображается в представлении. Может быть, я что-то упустил в publishResults...
Обновление
Из соображений совместимости я реализовал новый AddressLoader на основе Google Geocode Http API(поскольку служба геокодирования Android по умолчанию доступна не на всех устройствах). Он воспроизводит точно такое же странное поведение.