バックグラウンド情報
私が現在書いているアプリは場所を扱っています。場所を作成する主な方法は、住所を入力することです。それを行うための便利な方法を提供することは、UXにとって非常に重要です。
現在のソリューション
問題をしばらく調査した後、それを行うための最良の方法は、現在の場所とユーザー入力(Googleマップアプリケーションのように)に基づいてオートコンプリートを使用するテキスト領域であるという結論に達しました。この機能は残念ながらGoogleMapAPIによって提供されていないため、自分で実装する必要があります。
現在の実装
住所の提案を取得するには、Geocoderを使用します。これらの提案は、カスタムFilterを介してカスタムArrayAdaptorによってAutoCompleteTextViewに提供されます。
ArrayAdapter(一部のコードが削除されました)
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;
}
...
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<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;
}
...
問題
この実装は機能しますが、完全ではありません。
AddressLoaderは、いくつかの入力で奇妙な動作をします。
たとえば、ユーザーがこのアドレスを入力したい場合
10 rue Guy Ropartz 54600 Villers-les-Nancy
彼が入ったとき:
10 rue Guy
いくつかの提案がありますが、目的のアドレスではありません。
この入力に達したときに彼がテキストを入力し続ける場合:
10 rue Guy Ro
提案が消えます。入力が次の場合、最終的に目的のアドレスが表示されます
10 rue Guy Ropart
これはユーザーにとって不安なことだと思います。「10rueGuyRopart 」と「10rueGuyRopart」の提案があるのに、 「10rueGuyRo」の提案がないのは不思議です...
考えられる回避策
この問題の可能な回避策を想像し、それを実装しようとします。より長いアドレスを入力した後、AdressLoaderによって提案されたアドレスがない場合、以前の提案を保持するという考え方です。この例では、入力が「10 rue Guy Ro」の場合、「 10rueGuy 」の提案を保持します。
残念ながら、私のコードは機能しません。私がこの状況にあるとき、コードのかなりの部分に到達します:
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();
}
そして、performFilteringによって返されるFilterResultは良い提案で満たされていますが、ビューには表示されません。たぶん私はpublishResultsで何かを見逃しています...
質問
- ジオコーダーの動作がおかしい場合がありますが、住所の提案を取得するためのより良いアプローチがあるのではないでしょうか。
- 私が説明したものよりも良い回避策を提案できますか?
- 私の回避策の実装における問題は何ですか?
更新互換性の懸念から、 Google Geocode Http API
に基づいて新しいAddressLoaderを実装しました(デフォルトのAndroidジオコーダーサービスがすべてのデバイスで利用できるわけではないため)。まったく同じ奇妙な振る舞いを再現します。