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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?xml version="1.0" encoding="UTF-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:robobunny="http://robobunny.com" android:key="preference_screen"> <PreferenceCategory android:title="Speech"> <com.robobunny.SeekBarPreference android:key="speechRate" android:title="Speech speed" android:summary="Adjust reading speed" android:defaultValue="80" android:max="200" robobunny:min="1" robobunny:unitsLeft="" robobunny:unitsRight="%" /> </PreferenceCategory> </PreferenceScreen> |
The java class itself.
SeekBarPreference.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | 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); } } } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <?xml version="1.0" encoding="UTF-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/widget_frame" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="5dp" android:paddingLeft="15dp" android:paddingRight="10dp" android:paddingTop="5dp" > <TextView android:id="@+id/seekBarPrefUnitsRight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" > </TextView> <TextView android:id="@+id/seekBarPrefValue" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@id/seekBarPrefUnitsRight" android:gravity="right" > </TextView> <TextView android:id="@+id/seekBarPrefUnitsLeft" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@id/seekBarPrefValue" > </TextView> <LinearLayout android:id="@+id/seekBarPrefBarContainer" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/seekBarPrefUnitsRight" android:orientation="horizontal" > <SeekBar android:id="@+id/seekBarPrefSeekBar" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> </RelativeLayout> |
[…] Attention: This version of the SeekBar is obsolete, check out the new version instead […]
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:
2
3
4
5
6
7
8
9
10
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");
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.
2
3
4
5
6
7
8
9
10
11
12
13
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:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
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? 🙂
[…] that I can set from Android. As usual I searched the net for something that I can reuse and found a very nice implementation that suits my need . The first issue that came up while working with Preference Back End and Libgdx […]
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!
[…] bar preference class in the settings activity is based on this excellent post from Kirk Baucom, http://robobunny.com/wp/2013/08/24/android-seekbar-preference-v2/. The brilliant color preference selection class is […]
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:
(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