Wednesday, May 28, 2014

Dynamic auto-complete in Android

Auto-complete text input is very common in modern UI. It makes it easy to select an item from a large list or just provide some hint on matching items. Often the whole list of available items is not available, so you need to lookup matching items in some external source.
In this example we use auto-complete to select a stock, similar to the search field on finance.yahoo.com.

Here we use a web API from Yahoo to lookup matching stocks, but you can use the same approach with any way of populating the auto-complete drop-down dynamically. For example you could search in a database.

These are the major objects involved:
AutoCompleteTextView -> Adapter -> Filter



android.widget.AutoCompleteTextView is the standard Android widget for this purpose. We will use it as it is but will implement custom Adpter and Filter.

 symbolText = new AutoCompleteTextView(getActivity());
 symbolText.setAdapter(new StockLookupAdapter(getActivity()));

Here is our custom Adapter:

public class StockLookupAdapter extends
        ArrayAdapter<StockLookupAdapter.StockInfo> {

    private static final String LOG_TAG = StockLookupAdapter.class
            .getSimpleName();

    class StockInfo {
        public String symbol;
        public String name;
        public String exchange;

        @Override
        public String toString() {
            // text to display in the auto-complete dropdown
            return symbol + " (" + name + ")";
        }
    }

    private final StockLookupFilter filter = new StockLookupFilter();

    public StockLookupAdapter(Context context) {
        super(context, android.R.layout.simple_list_item_1);
    }

    @Override
    public Filter getFilter() {
        return filter;
    }

    private class StockLookupFilter extends Filter {
        ...
    }
}

Here StockInfo carries the data for each item in the drop down. Here we store the properties of each stock like symbol (a.k.a. ticker) and name. We override getFilter to return the custom Filter - StockLookupFilter. This is the essential part.
Here is what android.widget.Filter docu says:

Filtering operations performed by calling filter(CharSequence) or filter(CharSequence, android.widget.Filter.FilterListener) are performed asynchronously. When these methods are called, a filtering request is posted in a request queue and processed later. Any call to one of these methods will cancel any previous non-executed filtering request.

This is exactly what we need as calling a web API usually takes some time so we should not do it in the UI thread. Also the user may type faster than the web API can return the results. This could result in the hints shown in the drop-down lagging considerably behind the current text state. The queuing described above helps avoid this effect.

So here is our custom filter (nested inside StockLookupAdapter):
private class StockLookupFilter extends Filter {

    // Invoked in a worker thread to filter the data according to the
    // constraint.
    @Override
    protected FilterResults performFiltering(CharSequence constraint) {
        FilterResults results = new FilterResults();
        if (constraint != null) {
            ArrayList<StockInfo> list = lookupStock(constraint);
            results.values = list;
            results.count = list.size();
        }
        return results;
    }

    private ArrayList<StockInfo> lookupStock(CharSequence constraint) {
        ...
    }

    // Invoked in the UI thread to publish the filtering results in the user
    // interface.
    @Override
    protected void publishResults(CharSequence constraint,
            FilterResults results) {
        setNotifyOnChange(false);
        clear();
        if (results.count > 0) {
            addAll((ArrayList<StockInfo>) results.values);
            notifyDataSetChanged();
        } else {
            notifyDataSetInvalidated();
        }

    }

    @Override
    public CharSequence convertResultToString(Object resultValue) {
        if (resultValue instanceof StockInfo) {
            // text to set in the text view when an item from the dropdown
            // is selected
            return ((StockInfo) resultValue).symbol;
        }
        return null;
    }

}

perfromFiltering is executed in a background thread and it finds the items to be shown in the drop-down based on the current text in the text field.
publishResults is executed on the UI thread and it is given the FilterResults returned by perfromFiltering. Here we just reset the ArrayAdapter contents and notify the UI to update.
convertResultToString returns the string to be substituted in the text field when a given item from the drop-down is selected. In our case we display both stock symbol and name in the drop-down but want only the symbol in the text field.

So as we can see simple text navigation can be very efficient. Probably this is the reason why it is so popular these days.

P.S.
Still there is one glitch that irritates me. It seems part of the the drop-down is covered by the on-screen keyboard. If I try to close the keyboard, the drop-down is closed first.

No comments:

Post a Comment