Android. Выпадающий список (Spinner) с индикатором загрузки

в 16:41, , рубрики: android development, asynctask, Песочница, метки: ,

Приветствую тебя, читатель!

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

Собственно цель

Стандартный Spinner выглядит так:
Android. Выпадающий список (Spinner) с индикатором загрузки
После небольшой доработки получается нечто такое (CustomSpinner):
Android. Выпадающий список (Spinner) с индикатором загрузки

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

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

Android. Выпадающий список (Spinner) с индикатором загрузки

Для получения такого эффекта я вижу 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js