[Из песочницы] Android AutoCompleteTextView с подсказками из веб-сервиса

Для одного из своих Android-приложений Book Tracker я реализовал кастомный AutoCompleteTextView с подсказками для названий книг, которые динамически подгружаются с Google Books по мере ввода названия книги.Задача перед компонентом стояла следующая:

Загрузка данных должна осуществляться в отдельном потоке, чтобы не блокировать UI-поток; Загрузка подсказок должна начинаться только, если пользователь приостанавливает набор (чтобы предотвратить отправку множества запросов к серверу после каждого введенного символа); Подсказки должны загружаться, если пользователь ввел строку некоторой минимальной длины (нет смысла начинать загрузку данных для строки из двух или трех символов); При запросе к серверу в правой части поля должен быть показан анимированный прогресс, чтобы информировать пользователя о загрузке. Финальный результат: faeb356acddc40a39cc4ef8470edc5ab.gif

Шаг 1 — реализация кастомного адаптера для AutoCompleteTextViewАдаптер для AutoCompleteTextView — это ключевой компонент, в котором происходит загрузка и хранение подсказок. BookAutoCompleteAdapter реализовывает интерфейс Filterable, чтобы перехватывать ввод пользователя из AutoCompleteTextView и передавать его в качестве поискового запроса в веб-сервис. Единственный метод интерфейса Filterable — это getFilter (), который должен возвращать экземпляр класса Filter, осуществляющий загрузку и публикацию данных. Наследники класса Filter должны реализовать два метода: performFiltering (CharSequence constraint) и publishResults (CharSequence constraint, Filter.FilterResults results).Метод performFiltering будет вызван в отдельном потоке автоматически, поэтому нет необходимости создавать и запускать новый поток вручную. Это уже сделано за разработчика в классе Filter. Метод publishResults же вызывается в UI-потоке, чтобы опубликовать результаты на экране.

BookAutoCompleteAdapter.java public class BookAutoCompleteAdapter extends BaseAdapter implements Filterable { private static final int MAX_RESULTS = 10; private final Context mContext; private List mResults;

public BookAutoCompleteAdapter (Context context) { mContext = context; mResults = new ArrayList(); }

@Override public int getCount () { return mResults.size (); }

@Override public Book getItem (int index) { return mResults.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.from (mContext); convertView = inflater.inflate (R.layout.simple_dropdown_item_2line, parent, false); } Book book = getItem (position); ((TextView) convertView.findViewById (R.id.text1)).setText (book.getTitle ()); ((TextView) convertView.findViewById (R.id.text2)).setText (book.getAuthor ());

return convertView; }

@Override public Filter getFilter () { Filter filter = new Filter () { @Override protected FilterResults performFiltering (CharSequence constraint) { FilterResults filterResults = new FilterResults (); if (constraint!= null) { List books = findBooks (mContext, constraint.toString ()); // Assign the data to the FilterResults filterResults.values = books; filterResults.count = books.size (); } return filterResults; }

@Override protected void publishResults (CharSequence constraint, FilterResults results) { if (results!= null && results.count > 0) { mResults = (List) results.values; notifyDataSetChanged (); } else { notifyDataSetInvalidated (); } }};

return filter; }

/** * Returns a search result for the given book title. */ private List findBooks (String bookTitle) { // GoogleBooksService is a wrapper for the Google Books API GoogleBooksService service = new GoogleBooksService (mContext, MAX_RESULTS); return service.findBooks (bookTitle); } } Шаг 2 — создание XML-разметки для строки подсказки Когда подсказки загружены, будет показан выпадающий список с результатами. Каждая строка состоит из двух элементов: названия книги и имени автора.simple_dropdown_item_2line.xml

Шаг 3 — добавление задержки перед отправкой запроса на сервер При использовании стандартного AutoCompleteTextView запрос инициируется после каждого введенного символа. Если пользователь набирает текст без остановки, подсказки, полученные для предыдущего запроса, могут оказаться неактуальными при вводе каждого последующего символа. Это порождает ненужные и ресурсоемкие обращения к серверу, появляется шанс превышения лимитов API, которые может иметь веб-сервис, а также возвращаются устаревшие результаты, загруженные для предыдущего состояния строки запроса.Для того, чтобы избежать вышеописанных проблем, необходимо добавить небольшую задержку между вводом символа и отправкой запроса на сервер. Если во время этой задержки человек вводит следующий символ, запрос для предыдущей строки отменяется и переносится вперед на время задержки. Если же пользователь не изменяет строку на протяжении этого времени, запрос отправляется на сервер.

Чтобы реализовать вышеописанное поведение, нужно создать кастомную реализацию AutoCompleteTextView и переопределить метод performFiltering (CharSequence text, int keyCode). Поле mAutoCompleteDelay определяет время в миллисекундах, после которого запрос будет отправлен на сервер, если пользователь не ввел новых символов.

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); } } Шаг 4 — добавление анимированного прогресса к полю ввода Очень важно обеспечить обратную связь, когда пользователь набирает текст. Необходимо показать анимированный прогресс в поле ввода названия книги. Прогресс нужен для того, чтобы проинформировать человека о том, что подсказки загружаются и будут скоро отображены. Таким образом пользователь будет осведомлен и сможет подождать пока они не появятся. Без такой обратной связи человек может даже не подозревать о том, что поле может показывать подсказки.Элементы ProgressBar и DelayAutoCompleteTextView необходимо поместить во FrameLayout и выровнять ProgressBar по правой стороне родительской группы. Также необходимо изначально скрыть прогресс с помощью установки атрибута android: visibility=«gone».

ProgressBar подключается к DelayAutoCompleteTextView с помощью метода setLoadingIndicator (ProgressBar view) последнего. Видимость элемента прогресса устанавливается в View.VISIBLE, когда происходит загрузка подсказок и в View.GONE, когда загрузка завершена.

Шаг 5 — соединение компонентов Теперь, когда все части готовы, необходимо соединить их вместе: DelayAutoCompleteTextView bookTitle = (DelayAutoCompleteTextView) findViewById (R.id.book_title); bookTitle.setThreshold (4); bookTitle.setAdapter (new BookAutoCompleteAdapter (context)); bookTitle.setLoadingIndicator ((ProgressBar) findViewById (R.id.progress_bar)); bookTitle.setOnItemClickListener (new AdapterView.OnItemClickListener () { @Override public void onItemClick (AdapterView adapterView, View view, int position, long id) { Book book = (Book) adapterView.getItemAtPosition (position); bookTitle.setText (book.getTitle ()); } }); bookTitle.setThreshold (4) определяет минимальное количество символов, которые должен ввести пользователь, чтобы были показаны подсказки.

bookTitle.setLoadingIndicator ((ProgressBar) findViewById (R.id.progress_bar)) соединяет ProgressBar с DelayAutoCompleteTextView.

Важно установить OnItemClickListener для DelayAutoCompleteTextView и присвоить правильное значение полю ввода. Если этого не сделать, результат вызова метода toString () выбранного объекта будет вставлен в поле вместо названия книги.

© Habrahabr.ru