This guide will show you how implement a similar search box to the Google Maps app, using the built in AutoCompleteTextView, the Geocoder class, a custom Adapter and a few layout files.
The final result looks like:

Step 1 – Implement a custom Adapter to add results to the AutocompleteTextView
public class GeoAutoCompleteAdapter extends BaseAdapter implements Filterable {
private static final int MAX_RESULTS = 10;
private Context mContext;
private List resultList = new ArrayList();
public GeoAutoCompleteAdapter(Context context) {
mContext = context;
}
@Override
public int getCount() {
return resultList.size();
}
@Override
public GeoSearchResult getItem(int index) {
return resultList.get(index);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.geo_search_result_item, parent, false);
}
((TextView) convertView.findViewById(R.id.geo_search_result_text)).setText(getItem(position).getAddress());
return convertView;
}
@Override
public Filter getFilter() {
Filter filter = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null) {
List locations = findLocations(mContext, constraint.toString());
// Assign the data to the FilterResults
filterResults.values = locations;
filterResults.count = locations.size();
}
return filterResults;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
resultList = (List) results.values;
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
return filter;
}
private List<GeoSearchResult> findLocations(Context context, String query_text) {
List<GeoSearchResult> geo_search_results = new ArrayList<GeoSearchResult>();
Geocoder geocoder = new Geocoder(context, context.getResources().getConfiguration().locale);
List<Address> addresses = null;
try {
// Getting a maximum of 15 Address that matches the input text
addresses = geocoder.getFromLocationName(query_text, 15);
for(int i=0;i<addresses.size();i++){
Address address = (Address) addresses.get(i);
if(address.getMaxAddressLineIndex() != -1)
{
geo_search_results.add(new GeoSearchResult(address));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return geo_search_results;
}
} |
public class GeoAutoCompleteAdapter extends BaseAdapter implements Filterable {
private static final int MAX_RESULTS = 10;
private Context mContext;
private List resultList = new ArrayList();
public GeoAutoCompleteAdapter(Context context) {
mContext = context;
}
@Override
public int getCount() {
return resultList.size();
}
@Override
public GeoSearchResult getItem(int index) {
return resultList.get(index);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.geo_search_result_item, parent, false);
}
((TextView) convertView.findViewById(R.id.geo_search_result_text)).setText(getItem(position).getAddress());
return convertView;
}
@Override
public Filter getFilter() {
Filter filter = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null) {
List locations = findLocations(mContext, constraint.toString());
// Assign the data to the FilterResults
filterResults.values = locations;
filterResults.count = locations.size();
}
return filterResults;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
resultList = (List) results.values;
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
return filter;
}
private List<GeoSearchResult> findLocations(Context context, String query_text) {
List<GeoSearchResult> geo_search_results = new ArrayList<GeoSearchResult>();
Geocoder geocoder = new Geocoder(context, context.getResources().getConfiguration().locale);
List<Address> addresses = null;
try {
// Getting a maximum of 15 Address that matches the input text
addresses = geocoder.getFromLocationName(query_text, 15);
for(int i=0;i<addresses.size();i++){
Address address = (Address) addresses.get(i);
if(address.getMaxAddressLineIndex() != -1)
{
geo_search_results.add(new GeoSearchResult(address));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return geo_search_results;
}
}
This adapter class will handle the Geocoder search request as well as filtering and drawing the results in the dropdown menu. You’ll need to go ahead and create a new class in your project named GeoAutoCompleteAdapter.
The magic happens in the getFilter function which fires off the Geocoder request and returns the results.
Step 2 – Create a custom class to handle the result
I’ve used a custom class here to handle the Geocoder address result so that if you wanted to create some logic to perhaps format the string before it’s returned to the array adapter you can do so.
public class GeoSearchResult {
private Address address;
public GeoSearchResult(Address address)
{
this.address = address;
}
public String getAddress(){
String display_address = "";
display_address += address.getAddressLine(0) + "\n";
for(int i = 1; i < address.getMaxAddressLineIndex(); i++)
{
display_address += address.getAddressLine(i) + ", ";
}
display_address = display_address.substring(0, display_address.length() - 2);
return display_address;
}
public String toString(){
String display_address = "";
if(address.getFeatureName() != null)
{
display_address += address + ", ";
}
for(int i = 0; i < address.getMaxAddressLineIndex(); i++)
{
display_address += address.getAddressLine(i);
}
return display_address;
}
} |
public class GeoSearchResult {
private Address address;
public GeoSearchResult(Address address)
{
this.address = address;
}
public String getAddress(){
String display_address = "";
display_address += address.getAddressLine(0) + "\n";
for(int i = 1; i < address.getMaxAddressLineIndex(); i++)
{
display_address += address.getAddressLine(i) + ", ";
}
display_address = display_address.substring(0, display_address.length() - 2);
return display_address;
}
public String toString(){
String display_address = "";
if(address.getFeatureName() != null)
{
display_address += address + ", ";
}
for(int i = 0; i < address.getMaxAddressLineIndex(); i++)
{
display_address += address.getAddressLine(i);
}
return display_address;
}
}
Step 3 – Create the list item view for the results from the AutocompleteTextView
This view will be used for each result returned from the Geocoder request. Feel free to redesign this as you see fit.
Save this under res/layout/geo_search_result.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/geo_search_result_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp" /> |
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/geo_search_result_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp" />
Step 4 – Create a custom AutoCompleteTextView so you don’t spam the Geocoder
With a normal AutoCompleteTextView the filter function fires on every keypress. If you did this with an address on the Geocoder API the requests will be throttled and no results will be returned. So implementing a custom AutoCompleteTextView you can Override the performFiltering where we can use a Handler thread which fires after a delay period. If a request is fired before the Hander is complete the initial request is killed and a new request is formed.
Create a new class in your project named DelayAutoCompleteTextView.java
public class DelayAutoCompleteTextView extends AutoCompleteTextView {
private static final int MESSAGE_TEXT_CHANGED = 100;
private static final int DEFAULT_AUTOCOMPLETE_DELAY = 750;
private int mAutoCompleteDelay = DEFAULT_AUTOCOMPLETE_DELAY;
private ProgressBar mLoadingIndicator;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
DelayAutoCompleteTextView.super.performFiltering((CharSequence) msg.obj, msg.arg1);
}
};
public DelayAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setLoadingIndicator(ProgressBar progressBar) {
mLoadingIndicator = progressBar;
}
public void setAutoCompleteDelay(int autoCompleteDelay) {
mAutoCompleteDelay = autoCompleteDelay;
}
@Override
protected void performFiltering(CharSequence text, int keyCode) {
if (mLoadingIndicator != null) {
mLoadingIndicator.setVisibility(View.VISIBLE);
}
mHandler.removeMessages(MESSAGE_TEXT_CHANGED);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_TEXT_CHANGED, text), mAutoCompleteDelay);
}
@Override
public void onFilterComplete(int count) {
if (mLoadingIndicator != null) {
mLoadingIndicator.setVisibility(View.GONE);
}
super.onFilterComplete(count);
}
} |
public class DelayAutoCompleteTextView extends AutoCompleteTextView {
private static final int MESSAGE_TEXT_CHANGED = 100;
private static final int DEFAULT_AUTOCOMPLETE_DELAY = 750;
private int mAutoCompleteDelay = DEFAULT_AUTOCOMPLETE_DELAY;
private ProgressBar mLoadingIndicator;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
DelayAutoCompleteTextView.super.performFiltering((CharSequence) msg.obj, msg.arg1);
}
};
public DelayAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setLoadingIndicator(ProgressBar progressBar) {
mLoadingIndicator = progressBar;
}
public void setAutoCompleteDelay(int autoCompleteDelay) {
mAutoCompleteDelay = autoCompleteDelay;
}
@Override
protected void performFiltering(CharSequence text, int keyCode) {
if (mLoadingIndicator != null) {
mLoadingIndicator.setVisibility(View.VISIBLE);
}
mHandler.removeMessages(MESSAGE_TEXT_CHANGED);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_TEXT_CHANGED, text), mAutoCompleteDelay);
}
@Override
public void onFilterComplete(int count) {
if (mLoadingIndicator != null) {
mLoadingIndicator.setVisibility(View.GONE);
}
super.onFilterComplete(count);
}
}
Step 5 – Add the custom AutoCompleteTextView to your Activity’s layout file
In here I’ve used an X icon that allows the user to remove the current text inside the AutocompleteTextView. You can download this icon from here.
When you include your new custom component, use your own package name (e.g. com.yourthing.app)
Save this in your Activity’s layout, it contains the AutocompleteTextView and an ImageView containing the X icon for removing the text.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<YOUR_PACKAGE_NAME.DelayAutoCompleteTextView
android:id="@+id/geo_autocomplete"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:imeOptions="flagNoExtractUi|actionSearch"
android:inputType="textCapSentences"
android:textColor="@color/header_text_color"
android:background="@color/white"
android:hint="@string/map_geo_search_input_hint"
android:layout_gravity="center_vertical"
android:layout_marginEnd="35dp"
android:layout_marginRight="35dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:padding="10dp"
android:dropDownWidth="fill_parent"
/>
<ImageView
android:id="@+id/geo_autocomplete_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_action_remove"
android:contentDescription="@string/map_list_header_txt"
android:layout_gravity="center_vertical|end"
android:visibility="gone"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
/>
</FrameLayout> |
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<YOUR_PACKAGE_NAME.DelayAutoCompleteTextView
android:id="@+id/geo_autocomplete"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:imeOptions="flagNoExtractUi|actionSearch"
android:inputType="textCapSentences"
android:textColor="@color/header_text_color"
android:background="@color/white"
android:hint="@string/map_geo_search_input_hint"
android:layout_gravity="center_vertical"
android:layout_marginEnd="35dp"
android:layout_marginRight="35dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:padding="10dp"
android:dropDownWidth="fill_parent"
/>
<ImageView
android:id="@+id/geo_autocomplete_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_action_remove"
android:contentDescription="@string/map_list_header_txt"
android:layout_gravity="center_vertical|end"
android:visibility="gone"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
/>
</FrameLayout>
Step 6 – Assemble the components together
Add this code to the activity you wish to include the AutocompleteTextView and you should now have a Google Maps search box in your app!
private Integer THRESHOLD = 2;
private DelayAutoCompleteTextView geo_autocomplete;
private ImageView geo_autocomplete_clear;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_geocoder);
geo_autocomplete_clear = (ImageView) findViewById(R.id.geo_autocomplete_clear);
geo_autocomplete = (DelayAutoCompleteTextView) findViewById(R.id.geo_autocomplete);
geo_autocomplete.setThreshold(THRESHOLD);
geo_autocomplete.setAdapter(new GeoAutoCompleteAdapter(this)); // 'this' is Activity instance
geo_autocomplete.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
GeoSearchResult result = (GeoSearchResult) adapterView.getItemAtPosition(position);
geo_autocomplete.setText(result.getAddress());
}
});
geo_autocomplete.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
if(s.length() > 0)
{
geo_autocomplete_clear.setVisibility(View.VISIBLE);
}
else
{
geo_autocomplete_clear.setVisibility(View.GONE);
}
}
});
geo_autocomplete_clear.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
geo_autocomplete.setText("");
}
});
} |
private Integer THRESHOLD = 2;
private DelayAutoCompleteTextView geo_autocomplete;
private ImageView geo_autocomplete_clear;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_geocoder);
geo_autocomplete_clear = (ImageView) findViewById(R.id.geo_autocomplete_clear);
geo_autocomplete = (DelayAutoCompleteTextView) findViewById(R.id.geo_autocomplete);
geo_autocomplete.setThreshold(THRESHOLD);
geo_autocomplete.setAdapter(new GeoAutoCompleteAdapter(this)); // 'this' is Activity instance
geo_autocomplete.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
GeoSearchResult result = (GeoSearchResult) adapterView.getItemAtPosition(position);
geo_autocomplete.setText(result.getAddress());
}
});
geo_autocomplete.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
if(s.length() > 0)
{
geo_autocomplete_clear.setVisibility(View.VISIBLE);
}
else
{
geo_autocomplete_clear.setVisibility(View.GONE);
}
}
});
geo_autocomplete_clear.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
geo_autocomplete.setText("");
}
});
}
The majority of this codebase was developed by Alex Melnykov, I’ve just implemented the Geocoder search on top of this and added in an icon to clear the search box and format the results.