- PVSM.RU - https://www.pvsm.ru -
Для одного из своих Android-приложений Book Tracker [1] я реализовал кастомный AutoCompleteTextView [2] с подсказками для названий книг, которые динамически подгружаются с Google Books [3] по мере ввода названия книги.
Задача перед компонентом стояла следующая:
Финальный результат:

Адаптер для AutoCompleteTextView – это ключевой компонент, в котором происходит загрузка и хранение подсказок. BookAutoCompleteAdapter реализовывает интерфейс Filterable, чтобы перехватывать ввод пользователя из AutoCompleteTextView и передавать его в качестве поискового запроса в веб-сервис. Единственный метод интерфейса Filterable – это getFilter(), который должен возвращать экземпляр класса Filter, осуществляющий загрузку и публикацию данных. Наследники класса Filter должны реализовать два метода: performFiltering(CharSequence constraint) и publishResults(CharSequence constraint, Filter.FilterResults results).
Метод performFiltering будет вызван в отдельном потоке автоматически, поэтому нет необходимости создавать и запускать новый поток вручную. Это уже сделано за разработчика в классе Filter. Метод publishResults же вызывается в UI-потоке, чтобы опубликовать результаты на экране.
public class BookAutoCompleteAdapter extends BaseAdapter implements Filterable {
private static final int MAX_RESULTS = 10;
private final Context mContext;
private List<Book> mResults;
public BookAutoCompleteAdapter(Context context) {
mContext = context;
mResults = new ArrayList<Book>();
}
@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> 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<Books>) results.values;
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}};
return filter;
}
/**
* Returns a search result for the given book title.
*/
private List<Book> findBooks(String bookTitle) {
// GoogleBooksService is a wrapper for the Google Books API
GoogleBooksService service = new GoogleBooksService (mContext, MAX_RESULTS);
return service.findBooks(bookTitle);
}
}
Когда подсказки загружены, будет показан выпадающий список с результатами. Каждая строка состоит из двух элементов: названия книги и имени автора.
<?xml version="1.0" encoding="utf-8"?>
<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:mode="twoLine"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<TextView android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="?android:attr/textAppearanceLargePopupMenu"/>
<TextView android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/text1"
android:layout_alignStart="@id/text1"
android:layout_marginBottom="16dp"
android:textAppearance="?android:attr/textAppearanceSmall"/>
</TwoLineListItem>
При использовании стандартного AutoCompleteTextView запрос инициируется после каждого введенного символа. Если пользователь набирает текст без остановки, подсказки, полученные для предыдущего запроса, могут оказаться неактуальными при вводе каждого последующего символа. Это порождает ненужные и ресурсоемкие обращения к серверу, появляется шанс превышения лимитов API, которые может иметь веб-сервис, а также возвращаются устаревшие результаты, загруженные для предыдущего состояния строки запроса.
Для того, чтобы избежать вышеописанных проблем, необходимо добавить небольшую задержку между вводом символа и отправкой запроса на сервер. Если во время этой задержки человек вводит следующий символ, запрос для предыдущей строки отменяется и переносится вперед на время задержки. Если же пользователь не изменяет строку на протяжении этого времени, запрос отправляется на сервер.
Чтобы реализовать вышеописанное поведение, нужно создать кастомную реализацию AutoCompleteTextView и переопределить метод performFiltering(CharSequence text, int keyCode). Поле mAutoCompleteDelay определяет время в миллисекундах, после которого запрос будет отправлен на сервер, если пользователь не ввел новых символов.
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);
}
}
Очень важно обеспечить обратную связь, когда пользователь набирает текст. Необходимо показать анимированный прогресс в поле ввода названия книги. Прогресс нужен для того, чтобы проинформировать человека о том, что подсказки загружаются и будут скоро отображены. Таким образом пользователь будет осведомлен и сможет подождать пока они не появятся. Без такой обратной связи человек может даже не подозревать о том, что поле может показывать подсказки.
Элементы ProgressBar и DelayAutoCompleteTextView необходимо поместить во FrameLayout и выровнять ProgressBar по правой стороне родительской группы. Также необходимо изначально скрыть прогресс с помощью установки атрибута android:visibility=«gone».
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<com.melnykov.booktracker.ui.DelayAutoCompleteTextView
android:id="@+id/book_title"
android:inputType="textCapSentences"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="32dp"
android:imeOptions="flagNoExtractUi|actionSearch"/>
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:layout_marginRight="16dp"
android:visibility="gone"/>
</FrameLayout>
ProgressBar подключается к DelayAutoCompleteTextView с помощью метода setLoadingIndicator(ProgressBar view) последнего. Видимость элемента прогресса устанавливается в View.VISIBLE, когда происходит загрузка подсказок и в View.GONE, когда загрузка завершена.
Теперь, когда все части готовы, необходимо соединить их вместе:
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() выбранного объекта будет вставлен в поле вместо названия книги.
Автор: makovkastar
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/75136
Ссылки в тексте:
[1] Book Tracker: https://play.google.com/store/apps/details?id=com.melnykov.booktracker
[2] AutoCompleteTextView: http://developer.android.com/reference/android/widget/AutoCompleteTextView.html
[3] Google Books: https://developers.google.com/books/
[4] Источник: http://habrahabr.ru/post/243853/
Нажмите здесь для печати.