Приветствую тебя, читатель!
Представляю твоему вниманию небольшой очерк о том, как захотелось мне видеть прогресс-бар («infinite circle») во время загрузки данных в выпадающий список, который в Андроиде называется Spinner.
Необходимость такая возникла при разработке небольшой утилиты по работе с веб-сервисом. Параметры некоего расчёта хранятся на централизованном сервере. Веб-сервис .NET отдаёт списки возможных параметров в виде массивов разной длины (от 2 до 50 элементов). Для отображения этих параметров и был выбран выпадающий список. Инициализация списков, как и положено, происходит асинхронно. И в то время, пока данные загружаются, смотреть на пустые статичные элементы безо всякого прогресса скучно, уныло и вообще.
Стандартный Spinner выглядит так:

После небольшой доработки получается нечто такое (CustomSpinner):

«В чём же соль!?» — спросите вы? А соль в промежуточном состоянии (загрузка данных):

в светлой теме:

Для получения такого эффекта я вижу 2 пути:
1 Унаследоваться от Spinner; переопределить onDraw() и, возможно, какие-то другие методы; реализовать обработку состояний (загружается/загружен)
2 Унаследоваться от Layout; расположить на нём Spinner и ProgressBar; организовать работу контрола в соответствии со своими требованиями.
Первый путь, наверное, более правильный. Но каждый раз ступая на него я упирался в Exception'ы и вылеты приложения. Хотя с Android знаком достаточно давно, честно признаюсь — до сих пор не могу понять что именно нужно делаеть в методе onDraw(). Коврыться в исходниках очень не хочется, хотя иногда полезно. В-общем этот путь закончился, так тольком и не начавшись.
Второй путь подробнее описан в сети. По нему я прошёл быстро и непринуждённо. И был он таков…
Для начала нам нужно «набросать» наш новый контрол (custom_spinner.xml). В этом нет ничего сложного — корневой лэйаут и два дочерных элемента (спиннер и прогресс-бар). Для этого хорошо подойдёт RelativeLayout. У меня он получился вот таким:
Для работы с контролом нужно реализовать класс CustomSpinner. Создаём класс, унаследованный от RelativeLayout:
Для достижения первоначальной цели (показ прогресс-бара при загрузке данных) модифицируем класс CustomSpinner:
В случае когда контрол находится в процессе загрузки нужно скрыть возможные имеющиеся в нём значения списка —
Теперь при асинхронной загрузке, для которой я иcпользую AsyncTask, мы можем управлять поведением контрола:
Ну конечно, а куда ж без них!
Вспомним, что значально мы хотели именно Spinner. Поэтому поведение контрола должно быть соответствующим. При выбранной реализации нужно реализовать несколько затычек:
Методы
Внимание следует уделить методу
Оработчик выпадающего списка:
Если бы наш кастомный контрол был унаследован непосредственно от Spinner, то этих костылей могло бы и не быть. Но увы.
Осталось вставить наш новый элемент в интерфейс (лэйаут) приложения:
Таких контролов у меня на форме 25 штук. Когда-то вроде читал, что можно указывать нэймспейс своего приложения в заголовке лэйаута и тогда вроде не будет необходимости прописывать полное имя класса
Спасибо за внимание. Буду очень рад, если кто-нибудь даст ссылки на толковые статьи реализации такого контрола первым способом (унаследовать от Spinner). Или объяснит, как в методе onDraw() нарисовть прогресс-бар, а потом скрывать его при необходимости.
Представляю твоему вниманию небольшой очерк о том, как захотелось мне видеть прогресс-бар («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() нарисовть прогресс-бар, а потом скрывать его при необходимости.
