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

Назовем этот класс SeekBarPreference. Параметрами для него будут минимальное значение, максимальное значение и текущее значение по умолчанию, а реальное текущее значение он будет брать из ассоциированных настроек приложения по заданному ключу.
Тогда файл /res/xml/preferences.xml с разметкой настроек может выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<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 следует добавить описание атрибутов.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="com.mnm.seekbarpreference.SeekBarPreference">
<attr name="minValue" format="integer" />
<attr name="maxValue" format="integer" />
</declare-styleable>
</resources>
Атрибут name тега <declare-styleable> должен содержать квалифицированное имя класса нашего виджета.
Это же имя, только в более полном формате (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)
