Introduction
In this article, I’ll try to show how to create your own preference widget with SeekBar
inside.
Background
Android provides a number of handy widgets like CheckBoxPreference
, EditTextPreference
, ListPreference
to build nice preference screens. But if existing widgets somehow don’t fit your requirements, you can easily create your own based on existing.
Often some integer preferences have meaningful limitations like volume or brightness level. In this case, it makes sense to build your own widget to manage such preferences and then reuse this widget across applications.
Preparation
DialogPreference
is the best super class for such widgets. It provides a preference which shows two lines of text in preference list (title and summary) and shows dialog with Ok / Cancel buttons on click.
Let's call it SeekBarPreference
. Parameters for this widget can be minimum value, maximum value and current default value. Real current value will be stored in associated shared preferences by given key.
In this case, file /res/xml/preferences.xml can look like this:
<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>
Existing tag can be used to setup default current value. But minimum and maximum values need their own tags. For this /res/values/attrs.xml file should be extended with the next declaration:
="1.0"="utf-8"
<resources>
<declare-styleable name="com.mnm.seekbarpreference.SeekBarPreference">
<attr name="minValue" format="integer" />
<attr name="maxValue" format="integer" />
</declare-styleable>
</resources>
Name
attribute of <declare-styleable> tag should contain qualified widget class name.
The same name but in more full format (http://schemas.android.com/apk/res/qualified_class_name) should be defined in preferences.xml as additional namespace (see above).
The last stage of work with XML is the creation of layout for dialog launching on widget click. It is a really common code and could be omitted without any impact. Just mention that it contains minimum, maximum and current values’ TexViews
and SeekBar
.
Now, we can continue with SeekBarPreference
class implementation.
Implementation
At first, all mentioned attributes shall be read in widget constructor:
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);
where constants are names of namespaces, attributes and default values for attributes’ values (in case they are omitted in preference declaration):
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";
For dialog setup, onCreateDialogView
method should be overridden:
@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));
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;
}
Current value is gotten by key stated in preferences.xml for a particular widget instance. Also during SeekBar
setup should be taken into account that minimum value for it is zero. This is the reason why subtraction is used when minimum value of preference differs from zero. By the way, this code is correct for non-negative numbers only and when maximum value is really greater than minimum one.
After the previous step, the application can be launched and user can drag thumb but text on the current value label wouldn’t change because position change handler should be introduced.
For this purpose, SeekBarPreference
should implement OnSeekBarChangeListener
interface. In the previous code, exactly this interface was passed via “mSeekBar.setOnSeekBarChangeListener(this);
” call to SeekBar
. Only one method should be implemented from three possible:
public void onProgressChanged(SeekBar seek, int value, boolean fromTouch) {
mCurrentValue = value + mMinValue;
mValueText.setText(Integer.toString(mCurrentValue));
}
And again, addition should be used because SeekBar
minimum value is zero.
Next step is to persist changed value. On dialog close, it calls onDialogClosed
method that should be overridden:
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (!positiveResult) {
return;
}
if (shouldPersist()) {
persistInt(mCurrentValue);
}
notifyChanged();
}
On positive result value is persisted and shouldPersist()
check analyzes if it is necessary according to flag android:persistent
from preferences.xml.
Last line is needed for a small trick. The point is that by default, summary line of the widget is static
and if it should reflect current value, the next code should be added:
@Override
public CharSequence getSummary() {
String summary = super.getSummary().toString();
int value = getPersistedInt(mDefaultValue);
return String.format(summary, value);
}
Here on summary request, the original string
is treated like a template to put current value into it. It works perfectly on preferences screen open. But to get the same behavior after preference change via dialog, notifyChanged()
should be called.
Conclusion
Implemented widget is suitable for a wide range of preferences and it elegantly extends existing set of preference widgets. Dynamic summary line approach can be used across different preference widgets.
Links
History
- 1st March, 2011: Initial post