Monday, June 20, 2011

SeekBar в настройках приложения



Для создания экранов настроек Android предоставляет очень удобный набор виджетов, таких как CheckBoxPreference, EditTextPreference, ListPreference. В случае, если существующие виджеты по каким-либо причинам не соответствуют требованиям, можно создать свой собственный на базе существующих.

Довольно часто встречается ситуация, когда та или иная целочисленная настройка имеет разумные пределы: яркость, громкость и т.д. В этом случае имеет смысл создать собственный виджет, чтобы многократно использовать его в приложении.




Подготовка

За основу возьмем класс DialogPreference – базовый класс для виджетов, показывающих двухстрочный элемент в экране настроек и открывающих диалог при нажатии.



Назовем этот класс SeekBarPreference. Параметрами для него будут минимальное значение, максимальное значение и текущее значение по умолчанию, а реальное текущее значение он будет брать из ассоциированных настроек приложения по заданному ключу.

Тогда файл /res/xml/preferences.xml с разметкой настроек может выглядеть так:


<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:example="http://schemas.android.com/apk/res/com.mnm.seekbarpreference">


<com.mnm.seekbarpreference.SeekBarPreference
android:key="seekBarPreference"
android:title="@string/dialog_title"
android:dialogTitle="@string/dialog_title"
android:summary="@string/summary"
android:persistent="true"
android:defaultValue="20"
example:minValue="10"
example:maxValue="50" />


PreferenceScreen>

Для задания значения по умолчанию можно использовать существующий тег, а вот для минимального и максимального придется создать свои. Для этого в файл /res/values/attrs.xml следует добавить описание атрибутов.


<resources>
<declare-styleable name="com.mnm.seekbarpreference.SeekBarPreference">
<attr name="minValue" format="integer" />
<attr name="maxValue" format="integer" />
declare-styleable>
resources>

Атрибут name тега  должен содержать квалифицированное имя класса нашего виджета.

Это же имя, только в более полном формате (schemas.android.com/apk/res/квалифицированное_имя_класса) должно быть указано в разметке файла настроек как дополнительное пространство имен (см. выше).

Последний этап работы с xml – это создание разметки диалога, который будет вызываться по нажатию виджета. Код разметки не представляет из себя ничего необычного, поэтому может быть опущен без последствий. Он содержит TextView для минимального значения, максимального значения, текущего значения, и, собственно, SeekBar.

Теперь можно продолжить с реализацией класса SeekBarPreference.

Реализация

Для начала необходимо прочитать указанные значения из атрибутов в конструкторе:

mMinValue = attrs.getAttributeIntValue(PREFERENCE_NS, ATTR_MIN_VALUE, DEFAULT_MIN_VALUE);
mMaxValue = attrs.getAttributeIntValue(PREFERENCE_NS, ATTR_MAX_VALUE, DEFAULT_MAX_VALUE);
mDefaultValue = attrs.getAttributeIntValue(ANDROID_NS, ATTR_DEFAULT_VALUE, DEFAULT_CURRENT_VALUE);

где константы – это имена пространств имен, атрибутов и значения по умолчанию для минимума, максимума и значения по умолчанию (на случай, если они не будут указаны в разметке):

private static final String PREFERENCE_NS = "http://schemas.android.com/apk/res/com.mnm.seekbarpreference";
private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android";

private static final String ATTR_DEFAULT_VALUE = "defaultValue";
private static final String ATTR_MIN_VALUE = "minValue";
private static final String ATTR_MAX_VALUE = "maxValue";

Для инициализации диалога реализуем метод onCreateDialogView:

@Override
protected View onCreateDialogView() {

// Читаем значение из настроек
mCurrentValue = getPersistedInt(mDefaultValue);

// Создаем элемент
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.dialog_slider, null);

// Выставляем минимум и максимум
((TextView) view.findViewById(R.id.min_value)).setText(Integer.toString(mMinValue));
((TextView) view.findViewById(R.id.max_value)).setText(Integer.toString(mMaxValue));

// Настраиваем SeekBar
mSeekBar = (SeekBar) view.findViewById(R.id.seek_bar);
mSeekBar.setMax(mMaxValue - mMinValue);
mSeekBar.setProgress(mCurrentValue - mMinValue);
mSeekBar.setOnSeekBarChangeListener(this);

// Выставляем текущее значение
mValueText = (TextView) view.findViewById(R.id.current_value);
mValueText.setText(Integer.toString(mCurrentValue));

return view;
}

Значение читается по ключу, заданному в preferences.xml для виджета. При настройке SeekBar нужно учитывать, что для него минимальное значение – это всегда 0, поэтому приходится производить вычитание, если минимум отличен от нуля. Кстати, данный код верен только для неотрицательных чисел, а так же когда максимум больше минимума.

После этого уже можно запускать приложение и двигать ползунок, но текст текущего значения изменяться не будет, поскольку необходимо обработать изменения.

Для этого реализуем интерфейс OnSeekBarChangeListener у SeekBarPreference. В приведенном выше коде именно на этот интерфейс передается ссылка в mSeekBar.setOnSeekBarChangeListener(this). Необходимо реализовать только один метод из трех возможных:

public void onProgressChanged(SeekBar seek, int value, boolean fromTouch) {
mCurrentValue = value + mMinValue;
mValueText.setText(Integer.toString(mCurrentValue));
}

И опять же, из-за того, что минимальное значение SeekBar равно нулю, приходится применять сложение.

Следующий шаг – сохранение полученного значения. При применении или отмене изменений вызывается метод onDialogClosed, который и нужно переопределить:

@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);

if (!positiveResult) {
return;
}
if (shouldPersist()) {
persistInt(mCurrentValue);
}

notifyChanged();
}

При положительном варианте текущее значение сохраняется. Проверка shouldPersist() анализирует нужно ли это делать. При этом проверяется флаг android:persistent, указанный в preferences.xml.

Последняя строчка нужна для маленькой хитрости. Дело в том, что по умолчанию вторая строка виджета (summary) не динамическая, поэтому если хочется отображать в ней текущее значение, то необходимо добавить следующие строки:

@Override
public CharSequence getSummary() {
String summary = super.getSummary().toString();
int value = getPersistedInt(mDefaultValue);
return String.format(summary, value);
}

Здесь, при запросе summary, оригинальная строка выполняет роль шаблона, в который подставляется текущее значение. Это превосходно работает при открытии экрана настроек. Но чтобы заставить этот код работать после изменения значения, необходимо вызвать notifyChanged().

Результат

Полученный виджет подходит для применения к широкому спектру настроек и элегантно дополняет существующие виджеты. Подход с динамической строкой summary может использоваться и в других типах настроек.

Ссылки

Архив с проектом примера
Описание SeekBar (en)
Описание DialogPreference (en)

©HabraHabr.ru