Aug 242013
 

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:

SeekBar Preference v2


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:

  1. min: Minimum value to use for the slider. The default is zero
  2. unitsRight: The characters to put to the right of the value, to indicate the units (eg. %, deg, .oz)
  3. unitsLeft: The characters to put the left of the value

preferences.xml

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>

  35 Responses to “Android SeekBar preference v2”

  1. […] Attention: This version of the SeekBar is obsolete, check out the new version instead […]

  2. 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

  3. 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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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");
    • 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.

  4. 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

  5. thanks Mr. Kirk thank YOuuuuu SOuu much 🙂

  6. 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

  7. 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.

  8. 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!

  9. 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?

    • Thanks for your insights.I agree that gross income would be very misleading.I believe that it is better to do retirement planning based on retirement expenses rather than gross or net income which may or may not have any correlation to what is needed to sustain a retirement.

  10. 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?

  11. Setting the max in the xml from an integer resource doesn’t seem to work an isn’t possible programmatically

  12. 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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      @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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
      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;
      }
  13. 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.

  14. 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?

  15. 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;
    }

  16. 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!

  17. Does this work in API 8? I tried it but the slider keeps reverting to 0 and nothing gets set.

  18. Thanks for this, works flawlessly.

  19. How come the units do not show up? 🙂

  20. […] 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 […]

  21. Hey! thanks for great implementation.

    My only argument is that “presistInt(mCurrentValue)” should be called in the “onStopTrackingTouch” instead of “onProgressChanged”

  22. Hi, thanks for sharing!

  23. […] 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 […]

  24. 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.

  25. I fix my problem by importing
    import com.my.app.R;
    in the SeekBarPreference.java , but I not sure is that a right way.

  26. 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?

  27. Hi! Cool work!
    How i can localization robobunny:unitsRight?

  28. 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.

  29. Isn’t it supposed to be:

    1
          android:layout_below="@id/seekBarPrefUnitsRight"

    (Instead of “@+id”)

  30. @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/

  31. 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

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)