Aug 132011
While working on my Android implementation of the Dislexicon, I realized I would need a SeekBar preference to adjust the text-to-speech speed. I found a couple of SeekBar prefs online, but none of them fit my needs. Specifically, I wanted it to:
- Appear on the main preference screen, instead of a separate window accessed via a button
- Fill the entire width of the screen
- Allow a minimum value other than zero
Here’s what I ended up with, as it appears in Dislexicon:
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
- units: The characters to put to the right of the value, to indicate the units (eg. %, deg, .oz)
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 | 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.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; 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 ROBOBUNNYNS="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); } private void setValuesFromXml(AttributeSet attrs) { mMaxValue = attrs.getAttributeIntValue(ANDROIDNS, "max", 100); mMinValue = attrs.getAttributeIntValue(ROBOBUNNYNS, "min", 0); mUnitsLeft = getAttributeStringValue(attrs, ROBOBUNNYNS, "unitsLeft", ""); String units = getAttributeStringValue(attrs, ROBOBUNNYNS, "units", ""); mUnitsRight = getAttributeStringValue(attrs, ROBOBUNNYNS, "unitsRight", units); try { String newInterval = attrs.getAttributeValue(ROBOBUNNYNS, "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){ RelativeLayout layout = null; try { LayoutInflater mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); layout = (RelativeLayout)mInflater.inflate(R.layout.seek_bar_preference, parent, false); } catch(Exception e) { Log.e(TAG, "Error creating seek bar preference", e); } return layout; } @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()); } updateView(view); } /** * Update a SeekBarPreference view with our current state * @param view */ protected void updateView(View view) { try { RelativeLayout layout = (RelativeLayout)view; mStatusText = (TextView)layout.findViewById(R.id.seekBarPrefValue); mStatusText.setText(String.valueOf(mCurrentValue)); mStatusText.setMinimumWidth(30); mSeekBar.setProgress(mCurrentValue - mMinValue); TextView unitsRight = (TextView)layout.findViewById(R.id.seekBarPrefUnitsRight); unitsRight.setText(mUnitsRight); TextView unitsLeft = (TextView)layout.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; } } } |
The preference layout file. You can use this to tweek what the preference looks like.
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 49 50 51 52 53 54 55 56 57 58 59 60 61 | <?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:paddingLeft="15dp" android:paddingTop="5dp" android:paddingRight="10dp" android:paddingBottom="5dp" > <TextView android:id="@android:id/title" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="22dp" android:typeface="sans" android:textStyle="normal" android:textColor="#ffffff" ></TextView> <TextView android:id="@android:id/summary" android:layout_alignParentLeft="true" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@android:id/title" ></TextView> <TextView android:id="@+id/seekBarPrefUnitsRight" android:layout_alignParentRight="true" android:layout_below="@android:id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" ></TextView> <TextView android:id="@+id/seekBarPrefValue" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@id/seekBarPrefUnitsRight" android:layout_below="@android:id/title" android:gravity="right" ></TextView> <TextView android:id="@+id/seekBarPrefUnitsLeft" android:layout_below="@android:id/title" android:layout_toLeftOf="@id/seekBarPrefValue" android:layout_width="wrap_content" android:layout_height="wrap_content" ></TextView> <LinearLayout android:id="@+id/seekBarPrefBarContainer" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@android:id/summary"> </LinearLayout> </RelativeLayout> |


Great work ! Very nice and useful.
However I noticed that calling ‘notifyChanged();’ in ‘onProgressChanged’ prevents the proper tracking of the thumb. I suggest to update mStatusText only in ‘onProgressChanged’, and to actually change the preference value in ‘onStopTrackingTouch’. This will fix the tracking problem, and also will be a bit more efficient.
Thanks for the suggestion! I’ve updated the code.
FYI, setting a min value doesn’t seem to work. I have a value I want to go from 32 to 255 and the slider position moves incorrectly/erratically.
You’re right, there was a bug in the java code. The code that set the initial progress bar position was wrong, so the first time you moved it you’d get a seemingly random new value. I didn’t notice it because my min value was 1. I’ve updated the code. Thanks for the bug report, I hope the code is useful for you!
Kirk, glad you got it fixed. And yes, it’s very useful!
There are bug in this control when number of seekbars is more than one on single preference activity screen. Can your fix it?
It’s fixed now. I wasn’t handling the onBindView call correctly, so the views were getting switched around. I’ve tried to follow the method used by the built-in EditTextPreference, so hopefully it will behave itself. I made a couple of other small tweaks to make it work in a more “standard” fashion.
Thank you very much. it’s very useful.
However, I don’t know what should I do tracking of the thumb using dpad(left-right).
Could you inform to me?
I assumed you could just listen for them on the preference, but something seems to be grabbing keystrokes before they get there. I’ll post an update if I figure out how to do it.
Great! thank you very much. it’s good.
Thanks!
I am not sure if it works, cause if I use sharedpreferance with the same key in another place in my application it does not give value set within PreferanceActivity. Also if I set value outside PreferanceActivity with shared rpeference editor it doesn’t change value within SeekBarPreference.
Hmm, I’m not sure why that would be. I’m fetching the value in my app without any problems, but I’m not setting it outside the PreferenceActivity. Let me know if you find the issue.
Thanks a lot for sharing you class!
Thanks for writing this up in such detail — I learned a lot from it. I made a similar SeekBar in one of my projects and found that it ran fine in the android virtual device launched from Eclipse, but when I actually ran it on my phone, the app would crash as soon as I pressed the menu item that started up the Preferences screen. I fixed this issue by inverting the order of two lines in the initPreference method of the SeekBarPreference class. Here’s the order that worked for me:
mSeekBar.setMax(mMaxValue – mMinValue);
mSeekBar.setOnSeekBarChangeListener(this);
Thanks for the info! I haven’t seen that happen, but it probably depends on what your listener is doing. I’ve swapped the order as you suggested, since it should be safer.
Found two issues with this great code, thank you for that btw…
I’m using this in a preference activity, and in the androidmanifest.xml I declare a theme for the preference activity . This causes the seek bar to not display. Anyone else encounter this?
Also, another issue is the text and summary are not matching the default device theme colors, would be nice to have.
Thanks again!
JS
activity android:theme=”@android:style/Theme.Dialog
- Sorry that got culled out I guess, had to remove the opening less-than sign.
Hi, this is a little old but have you find a way to solve that problem ? I have the same and i don’t know how to do.
Thank you
Antoine
I have some kind problems with the seek bar, when touch on thumb to change value it disappears, when I release my finger it appears….
Also seek bar is “GONE”, when I have more then one element in preference category..
How to solve this?
Excellent code!
I found it while trying to get rid of my Lint Warnings for “unused” custom attributes.
Fell into a nasty little trap with the namespace because of an article on stackoverflow.com recommending
as a namespace in Java file and corresponding XML file, which results in an immediately crash. While
works fine.
Any ideas how this namespace tying Java code and XML stuff together really works?
Bests,
Tobias
I find this code doesn’t work well in diffirent version of Android. If works well in 2.3 and 4.0, But for 2.2.2 and 3.0 it doesn’t work well. How to solve this?
What behavior are you seeing?
The padding is not same as CheckBoxPreference and other exists Preference. I think the style maybe not right. But I can’t find how to solve this.
How to coop with an ‘android:dependancy’? The title stays white, althought the rest of the custom vies turns grey.
There are definitely problems with theming. I don’t know enough about Android UI theming to tell what’s going wrong, and I don’t have an actual Android device to test with (I just developed on the emulator). I’ve seen it behave differently on different devices depending on the phone vendor’s modifications.
If anyone can offer any tips, I’d appreciate it.
Hi Kirk,
Very good job on your Android tweak.
What I would like to point out to Android experts is that, some of us are not familiar with Android.
So, very clear “how to insert code” tutorials would be good. If you have the time, ofc.
However, the code worked beautifully for me. Even thou I do not understand everything in it!
nice tutorials, do you tell me how to build android application game tank step by step please, now i’m last semester in my study.please help me.
Hi Kirk,
I’m trying to use your seek bar preference component on a very simple preference list app.
The seek bar appears in a separate dialog Box and the value is never taken into account in the shared preference file… What am i doing wrong ??
It seems impossible to debug the component either, i put a breakpoint in every single method and execution never stops (i’m using an emulator).
i had a compilation problem also due to the Override key words that were placed before the overridden interface methods…
Thanks in advance
Really a very good work, very valuable, improved a lot avoid to using the arrays.
Very useful. Thanks!
Thanks so much for doing this but I am having some issue getting the SeekBar to show up. I can see the title, summary and value text but no seekbar. Is there something obvious I might be missing?
Thanks!
I’m not sure what would cause that. Are you getting any errors in your log? The seek bar gets added to the view on line 106 of SeekBarPreference.java. The first thing I’d try is setting a breakpoint there and stepping through to see if something is going wrong.
Yeah it’s definitely hitting it, and no, no errors. I tried adding it to a framelayout instead, but nothing. HierarchyViewer is not showing any views below the LinearLayout container (nor the Framelayout when I tried that).
Have you tried this with a PreferenceFragment? I’m just trying to figure out what I might be missing.
Oh it works with a PreferenceActivity…that’s weird.
Good work and thanks for sharing. However, if the seekbar preference is disabled, its value can still be changed. To prevent this form happening, simply override setEnabled() in SeekBarPreference.java:
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mSeekBar.setEnabled(enabled);
}
Best regards,
Dieter
Great work…Thanks a lot for sharing.:). Is the attached code contains all the bugfixes and improvements made by you. If not, where can I find the latest code.
why do i have to create a class for seekbar , i do not understand why can’t i just get the value
i need a full tutorial about this . and this is my question . i hope someone can answer
Here
This looks really cool, but after an hour or so of trying to compile it, I can’t figure out one line. In the preferences.xml, the 2nd line below for SeekBarPreference:
2
<com.robobunny.SeekBarPreference
Always fails with ‘error: Error parsing XML: unbound prefix’. Changing it to match my project name (like com.company.appname ) doesn’t fix it. I’ve tried many variations (and project cleans in-between), without success such as:
2
3
and
<com.company.appname.SeekBarPreference
Other than the 3 files, is there some additional project modifications necessary? I’d prefer not to have to name the project robobunny! Any suggestions would be appreciated, as I’d love to make this work.
There’s another name that needs to match between the code and the XML: the xmlns. I think that might be the problem you’re seeing.
The line in the XML that reads xmlns:robobunny=”http://robobunny.com” must match the string in the code:
private static final String ROBOBUNNYNS=”http://robobunny.com”;
(Obviously you can rename ROBOBUNNYNS if you do so throughout the code). Also, if you rename xmlns:robobunny to something else, you’ll need to also rename all the robobunny: strings in the XML. I think that’s probably what is wrong in your case from the error you’re getting.
Let me know if that doesn’t fix your problem.
When using the android:dependency field the only thing that gets greyed out is the summary. The Seekbar is also still useable.
Add this and your seekbars will respond to android:dependency:
@Override
public void onDependencyChanged(Preference dependency, boolean disableDependent)
{
super.onDependencyChanged(dependency, disableDependent);
// /see if it has been initialized
if (this.layout != null)
{
this.mSeekBar.setEnabled(!disableDependent);
this.mStatusText.setEnabled(!disableDependent);
}
}
Also add this in the onBindView(View view) method above updateView(View) to disable the seekbar when it first loads, otherwise you have to toggle the denpendency
if(!this.layout.isEnabled() && this.layout != null){
this.mSeekBar.setEnabled(false);
}
Thanks. I added it to bitbeaker. I had one problem when I tried to keep this class in it’s current package but I got it resolved by importing our package’s R class into SeekBarPreference.java
I had one other problem: summary was overlapping with seekBarPrefValue. I ended up adding
to summary. I had to include + sign before id or otherwise it just gave Error: No resource found that matches the given name. I have Android 2.3.6.
You didn’t have a license on your work. Is it alright to use this in a commercial android application?
Yes, you can consider it public domain. If you find any bugs or make any improvements, I’d appreciate it if you post them here.
Thanks much for sharing, Kirk,
With your permission, Google should include this in the next Android release, as it’s very useful indeed.
[...] classes I've seen floating around on the Internet (such as those by Hlidskialf and Robobunny), but is also designed with many best practice recommendations in mind (utilising advice from this [...]
Hi
I’m using your great seekbar in one of my apps.
Instead of creating the layout from XML for the preferences page I’m creating the page dynamically in code as it’s quite variable.
One issue I’ve come across is I’m not sure how to create a new instance of the seek bar with the AttributeSet attrs value. If using XML this gets automatically set. If I do it programatically how to I create it. If I just use the context – by changing the class a little, it mostly works, but then I get the indentation issue that I know you’ve referenced before here: http://stackoverflow.com/questions/5757012/custom-preference-targetsdkversion-11-missing-indent/14082812#14082812
What’s the best way I can use it in combination with dynamic screen creation?
I apologize that I can’t help you much, I don’t have very much experience with android GUI stuff myself. I created this widget for a program I was working on, but I haven’t used the built in android UI stuff subsequently. Hopefully someone else can answer your question.
BTW, the guy posting in the thread you referenced was not me, his post was just worded slightly confusingly. He seems to have a widget based on mine, he may be able to assist you.
Great piece of code, helped me a lot!
I want to ask one thing, though. How would I dynamically set the minimum value of a seekbar to be equal to the current value of another seekbar? Think Max speed / Min speed values, the min value should not be able to go above the max one.
“the min value should not be able to go above the max one.”
wrong words there, the max value should not be able to go below the min one
You’d have to add a couple new parameters to SeekBarPreference.java that can be updated externally, then check those new values in onProgressChanged to see if the user tried to make the setting lower or higher than is allowed. You can just return without setting anything (like the code in the middle of onProgressChanged). You’d also have to update the setting on the bar when something changes the min or max value. Then of course you’d have to have the two seekbars send updates back and forth.
It gets very tricky when you’re trying to link two bar settings together like that, especially for the user. The ideal solution would be to have two handles on one bar, so it’s obvious to the user what is going on when the handles won’t cross each other. You can also have one handle slide the other when they bump, or hit a minimum distance from each other. Many years ago I used something like that in a game for a bar used to allocate energy, and it worked out ok. That might actually be easier to do than making two seek bars talk to each other, too.
Thank you very much.
[...] with a HorizontalListView attached directly underneath it. I basically created it by modifying this code for a SeekBarPreference (which I am also using and is working fine). My ListViewPreference is [...]
Dude ur the best! This is exactly where I got stuck.
Thanks for sharing your code. This is a big help for me.
I added another ‘android:layout_toLeftOf=”@+id/seekBarPrefUnitsLeft”‘ to the summary-block in seek_bar_preference.xml. This way long summaries don’t overlap the prefUnit-stuff.