Приветствую тебя, читатель!
Представляю твоему вниманию небольшой очерк о том, как захотелось мне видеть прогресс-бар («infinite circle») во время загрузки данных в выпадающий список, который в Андроиде называется Spinner.
Необходимость такая возникла при разработке небольшой утилиты по работе с веб-сервисом. Параметры некоего расчёта хранятся на централизованном сервере. Веб-сервис .NET отдаёт списки возможных параметров в виде массивов разной длины (от 2 до 50 элементов). Для отображения этих параметров и был выбран выпадающий список. Инициализация списков, как и положено, происходит асинхронно. И в то время, пока данные загружаются, смотреть на пустые статичные элементы безо всякого прогресса скучно, уныло и вообще.
Собственно цель
Стандартный Spinner выглядит так:
После небольшой доработки получается нечто такое (CustomSpinner):
«В чём же соль!?» — спросите вы? А соль в промежуточном состоянии (загрузка данных):
в светлой теме:
Для получения такого эффекта я вижу 2 пути:
1 Унаследоваться от Spinner; переопределить onDraw() и, возможно, какие-то другие методы; реализовать обработку состояний (загружается/загружен)
2 Унаследоваться от Layout; расположить на нём Spinner и ProgressBar; организовать работу контрола в соответствии со своими требованиями.
Первый путь, наверное, более правильный. Но каждый раз ступая на него я упирался в Exception'ы и вылеты приложения. Хотя с Android знаком достаточно давно, честно признаюсь — до сих пор не могу понять что именно нужно делаеть в методе onDraw(). Коврыться в исходниках очень не хочется, хотя иногда полезно. В-общем этот путь закончился, так тольком и не начавшись.
Второй путь подробнее описан в сети. По нему я прошёл быстро и непринуждённо. И был он таков…
XML-layout
Для начала нам нужно «набросать» наш новый контрол (custom_spinner.xml). В этом нет ничего сложного — корневой лэйаут и два дочерных элемента (спиннер и прогресс-бар). Для этого хорошо подойдёт RelativeLayout. У меня он получился вот таким:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
<ProgressBar
android:id="@+id/progress"
style="@android:style/Widget.ProgressBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
Класс CustomSpinner
Для работы с контролом нужно реализовать класс CustomSpinner. Создаём класс, унаследованный от RelativeLayout:
public class CustomSpinner extends RelativeLayout {
Context context;
Spinner spinner;
ProgressBar progress;
public CustomSpinner(Context c, AttributeSet attrs) {
super(c, attrs);
this.context = c;
//Отрисовываем внещний вид из нашего ранее определённого ресурса custom_spinner.xml
LayoutInflater.from(context).inflate(R.layout.custom_spinner, this,
true);
initViews();
}
//Инициализируем дочерние контролы для дальнейшей работы с ними
private void initViews() {
spinner = (Spinner) findViewById(R.id.spinner);
progress = (ProgressBar) findViewById(R.id.progress);
}
}
Управление состоянием
Для достижения первоначальной цели (показ прогресс-бара при загрузке данных) модифицируем класс CustomSpinner:
public class CustomSpinner extends RelativeLayout {
Context context;
Spinner spinner;
ProgressBar progress;
ArrayAdapter<String> emptyAdapter;
public CustomSpinner(Context c, AttributeSet attrs) {
super(c, attrs);
this.context = c;
LayoutInflater.from(context).inflate(R.layout.custom_spinner, this,
true);
String[] strings = new String[] {};
List<String> items = new ArrayList<String>(Arrays.asList(strings));
emptyAdapter = new ArrayAdapter<String>(c,
android.R.layout.simple_spinner_item, items);
initViews();
}
private void initViews() {
spinner = (Spinner) findViewById(R.id.spinner);
progress = (ProgressBar) findViewById(R.id.progress);
}
public void loading(boolean flag) {
if (flag)
spinner.setAdapter(emptyAdapter);
progress.setVisibility(flag ? View.VISIBLE : View.GONE);
}
}
В случае когда контрол находится в процессе загрузки нужно скрыть возможные имеющиеся в нём значения списка — spinner.setAdapter(emptyAdapter);
. И, собственно, показать прогресс-бар.
Теперь при асинхронной загрузке, для которой я иcпользую AsyncTask, мы можем управлять поведением контрола:
CustomSpinner spinner;
...
@Override
protected void onPreExecute() {
spinner.loading(true);
}
...
@Override
protected SpinnerAdapter doInBackground(Map<String, Object>... params) {
//Здесь данные, полученные от веб-сервиса укладываются в SpinnerAdapter, которы впоследствии назначется нашему CustomSpinner
return null;
}
...
@Override
protected void onPostExecute(SpinnerAdapter result) {
spinner.loading(false);
if (result != null)
spinner.setAdapter(result);
}
«Костыли»
Ну конечно, а куда ж без них!
Вспомним, что значально мы хотели именно Spinner. Поэтому поведение контрола должно быть соответствующим. При выбранной реализации нужно реализовать несколько затычек:
public class CustomSpinner extends RelativeLayout {
...
//Обработчик события выбора значения выпадающего списка
public void setOnItemSelectedListener(OnItemSelectedListener l) {
spinner.setTag(getId());
spinner.setOnItemSelectedListener(l);
}
//Назначаем адаптер выпадающему списку
public void setAdapter(SpinnerAdapter adapter) {
spinner.setAdapter(adapter);
}
// Возвращаем индекс выбранного в списке элемента
public int getSelectedItemPosition() {
return this.spinner.getSelectedItemPosition();
}
//Получение адаптера выпадающего списка
public SpinnerAdapter getAdapter() {
return this.spinner.getAdapter();
}
}
Методы getAdapter(), setAdapter(), getSelectedItemPosition()
просто «пробрасывают» действия на внутренний Spinner.
Внимание следует уделить методу setOnItemSelectedListener(OnItemSelectedListener l)
. Я использую один обработчик (listener) для всех контролов (думаю так правильней) в котором с помощью switch(*some_unique_value*)...case(R.id.model)
определяю, что делать далее. Так как у выпадающего списка внутри нашего контрола нет уникального глобального идентификатора (он для всех R.id.spinner), то в тэг выпадающего списка записываем идентификатор родительского контрола (spinner.setTag(getId());
). Теперь при вызове обработчика смены значения в выпадающем списке сможем идентифицировать, какой именно список изменился:
Оработчик выпадающего списка:
class SpinnerItemSelectedListener implements OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> paramAdapterView,
View paramView, int paramInt, long paramLong) {
int id = ((SimpleParameter) paramAdapterView.getAdapter().getItem(
paramInt)).getId();
switch ((Integer) paramAdapterView.getTag()) {
case R.id.city:
initCityDependant(id);
break;
case ...:
otherMethod();
break;
default:
break;
}
}
@Override
public void onNothingSelected(AdapterView<?> paramAdapterView) {
}
}
Если бы наш кастомный контрол был унаследован непосредственно от Spinner, то этих костылей могло бы и не быть. Но увы.
CustomSpinner на форме
Осталось вставить наш новый элемент в интерфейс (лэйаут) приложения:
<org.solo12zw74.app.CustomSpinner
android:id="@+id/model"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
Таких контролов у меня на форме 25 штук. Когда-то вроде читал, что можно указывать нэймспейс своего приложения в заголовке лэйаута и тогда вроде не будет необходимости прописывать полное имя класса org.solo12zw74.app.CustomSpinner
. Но сейчас почему-то не получилось.
Послесловие
Спасибо за внимание. Буду очень рад, если кто-нибудь даст ссылки на толковые статьи реализации такого контрола первым способом (унаследовать от Spinner). Или объяснит, как в методе onDraw() нарисовть прогресс-бар, а потом скрывать его при необходимости.
Автор: solo12zw74