Для создания экранов настроек 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)