A couple of years ago, I posted an Android SeekBar preference widget that I’d written. Since then, people have regularly posted fixes and enhancements. This is a new version that includes those fixes. I decided to make a new post because the thread on the old one was getting a bit long, and hopefully those problems will be gone now. Special thanks to Yair from KwazyLabs for layout updates that should make the widget behave itself when the theme is not the default.
Here’s v2 of the preference, as it appears in Dislexicon:
Visually, the widget is the same as the old one, except that the summary is no longer on the same line as the units. Thanks to everyone who helped out, let me know if you find any problems or have ideas on how to improve the widget.
You’ll need two files, plus a preferences section. They are all inline below, but you can download them here:
File | Description | |
---|---|---|
preferences.xml | Example of adding a config to your preferences. | |
SeekBarPreference.java | The java class. | |
seek_bar_preference.xml | The XML layout of the preference. This should go in your res/layout directory. |
Here’s an example of configuring it in your preferences.xml file. Most of the parameters work exactly the same as a standard preference, but there are two new parameters:
- min: Minimum value to use for the slider. The default is zero
- unitsRight: The characters to put to the right of the value, to indicate the units (eg. %, deg, .oz)
- unitsLeft: The characters to put the left of the value
preferences.xml
[cc lang=”xml”]
[/cc]
The java class itself.
SeekBarPreference.java
[cc lang=”java”]
package com.robobunny;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
public class SeekBarPreference extends Preference implements OnSeekBarChangeListener {
private final String TAG = getClass().getName();
private static final String ANDROIDNS=”http://schemas.android.com/apk/res/android”;
private static final String APPLICATIONNS=”http://robobunny.com”;
private static final int DEFAULT_VALUE = 50;
private int mMaxValue = 100;
private int mMinValue = 0;
private int mInterval = 1;
private int mCurrentValue;
private String mUnitsLeft = “”;
private String mUnitsRight = “”;
private SeekBar mSeekBar;
private TextView mStatusText;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
initPreference(context, attrs);
}
public SeekBarPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initPreference(context, attrs);
}
private void initPreference(Context context, AttributeSet attrs) {
setValuesFromXml(attrs);
mSeekBar = new SeekBar(context, attrs);
mSeekBar.setMax(mMaxValue – mMinValue);
mSeekBar.setOnSeekBarChangeListener(this);
setWidgetLayoutResource(R.layout.seek_bar_preference);
}
private void setValuesFromXml(AttributeSet attrs) {
mMaxValue = attrs.getAttributeIntValue(ANDROIDNS, “max”, 100);
mMinValue = attrs.getAttributeIntValue(APPLICATIONNS, “min”, 0);
mUnitsLeft = getAttributeStringValue(attrs, APPLICATIONNS, “unitsLeft”, “”);
String units = getAttributeStringValue(attrs, APPLICATIONNS, “units”, “”);
mUnitsRight = getAttributeStringValue(attrs, APPLICATIONNS, “unitsRight”, units);
try {
String newInterval = attrs.getAttributeValue(APPLICATIONNS, “interval”);
if(newInterval != null)
mInterval = Integer.parseInt(newInterval);
}
catch(Exception e) {
Log.e(TAG, “Invalid interval value”, e);
}
}
private String getAttributeStringValue(AttributeSet attrs, String namespace, String name, String defaultValue) {
String value = attrs.getAttributeValue(namespace, name);
if(value == null)
value = defaultValue;
return value;
}
@Override
protected View onCreateView(ViewGroup parent) {
View view = super.onCreateView(parent);
// The basic preference layout puts the widget frame to the right of the title and summary,
// so we need to change it a bit – the seekbar should be under them.
LinearLayout layout = (LinearLayout) view;
layout.setOrientation(LinearLayout.VERTICAL);
return view;
}
@Override
public void onBindView(View view) {
super.onBindView(view);
try {
// move our seekbar to the new view we’ve been given
ViewParent oldContainer = mSeekBar.getParent();
ViewGroup newContainer = (ViewGroup) view.findViewById(R.id.seekBarPrefBarContainer);
if (oldContainer != newContainer) {
// remove the seekbar from the old view
if (oldContainer != null) {
((ViewGroup) oldContainer).removeView(mSeekBar);
}
// remove the existing seekbar (there may not be one) and add ours
newContainer.removeAllViews();
newContainer.addView(mSeekBar, ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
catch(Exception ex) {
Log.e(TAG, “Error binding view: ” + ex.toString());
}
//if dependency is false from the beginning, disable the seek bar
if (view != null && !view.isEnabled())
{
mSeekBar.setEnabled(false);
}
updateView(view);
}
/**
* Update a SeekBarPreference view with our current state
* @param view
*/
protected void updateView(View view) {
try {
mStatusText = (TextView) view.findViewById(R.id.seekBarPrefValue);
mStatusText.setText(String.valueOf(mCurrentValue));
mStatusText.setMinimumWidth(30);
mSeekBar.setProgress(mCurrentValue – mMinValue);
TextView unitsRight = (TextView)view.findViewById(R.id.seekBarPrefUnitsRight);
unitsRight.setText(mUnitsRight);
TextView unitsLeft = (TextView)view.findViewById(R.id.seekBarPrefUnitsLeft);
unitsLeft.setText(mUnitsLeft);
}
catch(Exception e) {
Log.e(TAG, “Error updating seek bar preference”, e);
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int newValue = progress + mMinValue;
if(newValue > mMaxValue)
newValue = mMaxValue;
else if(newValue < mMinValue)
newValue = mMinValue;
else if(mInterval != 1 && newValue % mInterval != 0)
newValue = Math.round(((float)newValue)/mInterval)*mInterval;
// change rejected, revert to the previous value
if(!callChangeListener(newValue)){
seekBar.setProgress(mCurrentValue - mMinValue);
return;
}
// change accepted, store it
mCurrentValue = newValue;
mStatusText.setText(String.valueOf(newValue));
persistInt(newValue);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
notifyChanged();
}
@Override
protected Object onGetDefaultValue(TypedArray ta, int index){
int defaultValue = ta.getInt(index, DEFAULT_VALUE);
return defaultValue;
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
if(restoreValue) {
mCurrentValue = getPersistedInt(mCurrentValue);
}
else {
int temp = 0;
try {
temp = (Integer)defaultValue;
}
catch(Exception ex) {
Log.e(TAG, "Invalid default value: " + defaultValue.toString());
}
persistInt(temp);
mCurrentValue = temp;
}
}
/**
* make sure that the seekbar is disabled if the preference is disabled
*/
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mSeekBar.setEnabled(enabled);
}
@Override
public void onDependencyChanged(Preference dependency, boolean disableDependent) {
super.onDependencyChanged(dependency, disableDependent);
//Disable movement of seek bar when dependency is false
if (mSeekBar != null)
{
mSeekBar.setEnabled(!disableDependent);
}
}
}
[/cc]
The preference layout file. You can use this to tweek what the preference looks like. You should put this in your res/layout directory.
seek_bar_preference.xml
[cc lang=”xml”]
[/cc]
hi sir.. thanks for the code… BTW its help me a lot..
there’s no error .. but i when run my application.. theres nothing happen??
even i change the speed rate.. please help me..
is these code automatically change the Speed Rate of TTS?????
Android: Version – 4.1.2
RAM
No, it’s just a generic SeekBar. I happen to use it in my program to adjust the speech speed, but you’ll have to implement the piece of code that does something with the value yourself. Here’s an example:
[cc lang=”java”]
// get the prefs object. I do this in the onCreate method of my Activity
SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
// get the value from the seekbar
mSpeechRate = mPrefs.getInt(“speechRate”, mSpeechRate);
// speech speed is a percentage relative to “normal” speed
float rate = ((float)mSpeechRate / 100f);
// set the speed and see if it worked
int status = mTts.setSpeechRate(rate);
if(status != TextToSpeech.SUCCESS)
Log.e(TAG, “Error setting speech speed”);
[/cc]
Hi.
The above code snippet doesn’t seem to work.
Would you append to your main post, an example of how to actually USE the seekbar in an activity, like, when te value is changed, something happens, etc etc.
Hi Robobunny,
i found your code, which works perfect. I just noticed that maybe there’s better to move line 173:
persistInt(newValue);
from onProgressChanged() to onStopTrackingTouch(). If you listen for a preference change, there will be a lot of code executed useless, i guess.
and a question: can i freely use your code, or pretend i made it by myself? 😀
cheers, Ales
thanks Mr. Kirk thank YOuuuuu SOuu much 🙂
mr. kirk help me again…
i put ur given CODE on my OnCreate method of my activity, VoiceRecogniton where TTS activated. (different from your SeekBarPreference.java)
i got the error from this Code “float rate = ((float)mSpeechRate / 100f);”
i declare int mSpeechRate = 0; correct me please if im wrong…
if you like mr. kirk and im happy if you share the project..to me 🙂
thank you mr. kirk sorry im a newBIE
Very useful piece of code. Thanks
I would like to use this class in my own project. What is the license under you provide this code?
Regards.
Public domain
Hey Bro! Thanks for this!
It has been useful to me in all my apps!
And I guess this update will be even better.
U rock!
Hello,
I found a bug, when ever I create a preference activty with this theme:
android:theme=”@android:style/Theme.DeviceDefault.Dialog” >
when it is in dialog the seek bar doesnt appear?
Do you know how to solve this?
I tried the code as is, and it works great!
I would like to have the seekbar to display float variables. (ie. 4.50%, 5.25%, 6.00% etc)
I’ve been trying to modify this code for hours with no luck. Anyone can help me out?
Setting the max in the xml from an integer resource doesn’t seem to work an isn’t possible programmatically
Hi,
Your code helps a lot, but it contains a bug. It produces the following error/warning for every seek bar position change (at least under Android 4.3): requestLayout() improperly called by android.widget.LinearLayout … during layout: running second layout pass. This happens due to the fact that you create new SeekBar for every binding. I don’t know why do you do this at all. You do already have a seek bar instance defined in xml-layout. Why not to use it directly? Here is a simplified version of related code which is free from the error.
@Override
public void onBindView(View view)
{
super.onBindView(view);
if(view != null)
{
mSeekBar = (SeekBar)view.findViewById(R.id.seekBarPrefSeekBar);
mSeekBar.setMax(mMaxValue - mMinValue);
mSeekBar.setOnSeekBarChangeListener(this);
}
updateView(view);
}
This works smoothly. Also you should probably improve getAttributeStringValue method to support string resources resolving:
private String getAttributeStringValue(AttributeSet attrs, String namespace, String name, String defaultValue)
{
final String STR = "@string/";
String value = attrs.getAttributeValue(namespace, name);
if(value == null)
value = defaultValue;
if(value.length() > 1 && value.charAt(0) == '@' && value.contains(STR))
{
Resources res = owner.getResources();
final int id = res.getIdentifier(owner.getPackageName() + ":" + value.substring(1), null, null);
value = owner.getString(id);
}
return value;
}
I found a bug that should be easily fixable, when the preference is diabled “android:enabled=”false”” the seek bar is still active.
PS Ive also added some code so that you have the option to disable the units view, if anyone is interested, let me know.
The Case of the Disappearing SeekBar
When my activity is created, the actual seekbar is missing from the preference! I added mSeekBar.setEnabled(true) in initPreference, and I can see it flicker. It reappears and stays when I toggle any other CheckBoxPreference in the activity. What on earth is going on?
Stan,
Thanks for that, it seems to help my issue a lot! I’m assuming owner is the same as context, so I changed your code a bit for the second value:
private String getAttributeStringValue(AttributeSet attrs, String namespace, String name, String defaultValue)
{
final String STR = “@string/”;
String value = attrs.getAttributeValue(namespace, name);
if(value == null)
value = defaultValue;
if(value.length() > 1 && value.charAt(0) == ‘@’ && value.contains(STR))
{
Context context=getContext();
Resources res = context.getResources();
final int id = res.getIdentifier(context.getPackageName() + “:” + value.substring(1), null, null);
value = context.getString(id);
}
return value;
}
First of all, I wanna thank you for sharing your code!
I was able to implement it in my application, but I actually still don’t get how I can listen to the changing slider value. When I use the OnPreferenceChangeListener, my slider becomes disabled and cannot be moved anymore.. Could you please give me a hint how to exactly use your code to get the current slider value ?
That would be really great!!
Thanks forwards!
Does this work in API 8? I tried it but the slider keeps reverting to 0 and nothing gets set.
Thanks for this, works flawlessly.
How come the units do not show up? 🙂
Hey! thanks for great implementation.
My only argument is that “presistInt(mCurrentValue)” should be called in the “onStopTrackingTouch” instead of “onProgressChanged”
Hi, thanks for sharing!
In the source directory src I created directory com/robobunny and copy downloaded SeekBarPreference.java into it.
src/com/my/…
src/com/robobunny/SeekBarPreference.java
res/layout/seek_bar_preference.xml
res/xml/preferences.xml
But Eclipse reports an error R.layout.seek_bar_preference and points into SeekBarPreference.java.
There is no errors in res/layout/seek_bar_preference.xml and res/xml/preferences.xml.
I tried to clean and rebuild project, exit from Eclipse and start it again, delete gen folder – the result is the same.
I fix my problem by importing
import com.my.app.R;
in the SeekBarPreference.java , but I not sure is that a right way.
I have more questions 🙂
1) As I can see, robobunny:unitsLeft and robobunny:unitsRight doesn’t support localization, values like @string/id displaed as is, isn’t it?
2) How I can change spinner limit in runtime (or, if possible, in design time) in case when one spinner limits depened on another spinner value?
Hi! Cool work!
How i can localization robobunny:unitsRight?
Does anyone get the anomaly whereby the SeekBar’s heading text is not greyed out when the SeekBar is disabled? I don’t know how to resolve this.
Is the SeekBar example missing some components?
Never mind, I figured it out.
Just add codes in the “updateView” event to change the colour of the TextViews to whatever colour you want. Maybe the poster needs to update his tutorial to show how this can be accomplished for new readers.
Isn’t it supposed to be:
android:layout_below="@id/seekBarPrefUnitsRight"
(Instead of “@+id”)
@Everyone who’s looking for proper localization and getting integer values from resources, this blog shows how it’s done properly: http://kevindion.com/2011/01/custom-xml-attributes-for-android-widgets/
Hello,
thanks a lot for your control.
I migrated it to Xamarin in one of my projects, but would like to ask you, if you mind, if I put it to the github.
I will add there reference to this article.
thx a lot
Rado