{"id":190,"date":"2013-08-24T01:47:19","date_gmt":"2013-08-24T05:47:19","guid":{"rendered":"http:\/\/robobunny.com\/wp\/?p=190"},"modified":"2013-08-24T01:47:19","modified_gmt":"2013-08-24T05:47:19","slug":"android-seekbar-preference-v2","status":"publish","type":"post","link":"https:\/\/robobunny.com\/wp\/?p=190","title":{"rendered":"Android SeekBar preference v2"},"content":{"rendered":"<p>A couple of years ago, I <a href=\"\/wp\/2011\/08\/13\/android-seekbar-preference\/\">posted an Android SeekBar preference widget<\/a> that I&#8217;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 <a href=\"http:\/\/kwazylabs.com\">Yair from KwazyLabs<\/a> for layout updates that should make the widget behave itself when the theme is not the default.<\/p>\n<p>Here&#8217;s v2 of the preference, as it appears in Dislexicon:<\/p>\n<p><a href=\"\/wp\/wp-content\/uploads\/2013\/08\/seekbar_preview.png\"><img loading=\"lazy\" src=\"\/wp\/wp-content\/uploads\/2013\/08\/seekbar_preview.png\" alt=\"SeekBar Preference v2\" width=\"487\" height=\"189\" class=\"alignnone size-full wp-image-191\" srcset=\"https:\/\/robobunny.com\/wp\/wp-content\/uploads\/2013\/08\/seekbar_preview.png 487w, https:\/\/robobunny.com\/wp\/wp-content\/uploads\/2013\/08\/seekbar_preview-300x116.png 300w\" sizes=\"(max-width: 487px) 100vw, 487px\" \/><\/a><\/p>\n<p><!--more--><br \/>\nVisually, 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.<\/p>\n<p>You&#8217;ll need two files, plus a preferences section. They are all inline below, but you can download them here:<\/p>\n<table class=\"highlight-box\" width=\"100%\" cellspacing=\"2\" cellpadding=\"2\">\n<thead>\n<tr>\n<th>File<\/th>\n<th><\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><a href=\"\/blog_files\/android_seekbar_preference\/preferences.xml\">preferences.xml<\/a><\/td>\n<td style=\"width: 10px;\"><\/td>\n<td>Example of adding a config to your preferences.<\/td>\n<\/tr>\n<tr>\n<td><a href=\"\/blog_files\/android_seekbar_preference\/SeekBarPreference.java\">SeekBarPreference.java<\/a><\/td>\n<td style=\"width: 10px;\"><\/td>\n<td>The java class.<\/td>\n<\/tr>\n<tr>\n<td><a href=\"\/blog_files\/android_seekbar_preference\/seek_bar_preference.xml\">seek_bar_preference.xml<\/a><\/td>\n<td style=\"width: 10px;\"><\/td>\n<td>The XML layout of the preference. This should go in your res\/layout directory.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>&nbsp;<\/p>\n<p>Here&#8217;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:<\/p>\n<ol>\n<li>min: Minimum value to use for the slider. The default is zero<\/li>\n<li>unitsRight: The characters to put to the right of the value, to indicate the units (eg. %, deg, .oz)<\/li>\n<li>unitsLeft: The characters to put the left of the value<\/li>\n<\/ol>\n<p><a href=\"\/blog_files\/android_seekbar_preference\/preferences.xml\">preferences.xml<\/a><br \/>\n[cc lang=&#8221;xml&#8221;]<br \/>\n<?xml version=\"1.0\" encoding=\"UTF-8\"?><\/p>\n<p><PreferenceScreen\n\txmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n\txmlns:robobunny=\"http:\/\/robobunny.com\"\n\tandroid:key=\"preference_screen\"><\/p>\n<p>\t<PreferenceCategory android:title=\"Speech\"><br \/>\n\t\t<com.robobunny.SeekBarPreference\n\t\t\tandroid:key=\"speechRate\"\n\t\t\tandroid:title=\"Speech speed\"\n\t\t\tandroid:summary=\"Adjust reading speed\"\n\t\t\tandroid:defaultValue=\"80\"\n\t\t\tandroid:max=\"200\"\n\t\t\trobobunny:min=\"1\"\n\t\t\trobobunny:unitsLeft=\"\"\n\t\t\trobobunny:unitsRight=\"%\"\n\t\/><br \/>\n\t<\/PreferenceCategory><\/p>\n<p><\/PreferenceScreen><br \/>\n[\/cc]<\/p>\n<p>The java class itself.<br \/>\n<a href=\"\/blog_files\/android_seekbar_preference\/SeekBarPreference.java\">SeekBarPreference.java<\/a><br \/>\n[cc lang=&#8221;java&#8221;]<br \/>\npackage com.robobunny;<\/p>\n<p>import android.content.Context;<br \/>\nimport android.content.res.TypedArray;<br \/>\nimport android.preference.Preference;<br \/>\nimport android.util.AttributeSet;<br \/>\nimport android.util.Log;<br \/>\nimport android.view.View;<br \/>\nimport android.view.ViewGroup;<br \/>\nimport android.view.ViewParent;<br \/>\nimport android.widget.LinearLayout;<br \/>\nimport android.widget.RelativeLayout;<br \/>\nimport android.widget.SeekBar;<br \/>\nimport android.widget.SeekBar.OnSeekBarChangeListener;<br \/>\nimport android.widget.TextView;<\/p>\n<p>public class SeekBarPreference extends Preference implements OnSeekBarChangeListener {<\/p>\n<p>\tprivate final String TAG = getClass().getName();<\/p>\n<p>\tprivate static final String ANDROIDNS=&#8221;http:\/\/schemas.android.com\/apk\/res\/android&#8221;;<br \/>\n\tprivate static final String APPLICATIONNS=&#8221;http:\/\/robobunny.com&#8221;;<br \/>\n\tprivate static final int DEFAULT_VALUE = 50;<\/p>\n<p>\tprivate int mMaxValue      = 100;<br \/>\n\tprivate int mMinValue      = 0;<br \/>\n\tprivate int mInterval      = 1;<br \/>\n\tprivate int mCurrentValue;<br \/>\n\tprivate String mUnitsLeft  = &#8220;&#8221;;<br \/>\n\tprivate String mUnitsRight = &#8220;&#8221;;<br \/>\n\tprivate SeekBar mSeekBar;<\/p>\n<p>\tprivate TextView mStatusText;<\/p>\n<p>\tpublic SeekBarPreference(Context context, AttributeSet attrs) {<br \/>\n\t\tsuper(context, attrs);<br \/>\n\t\tinitPreference(context, attrs);<br \/>\n\t}<\/p>\n<p>\tpublic SeekBarPreference(Context context, AttributeSet attrs, int defStyle) {<br \/>\n\t\tsuper(context, attrs, defStyle);<br \/>\n\t\tinitPreference(context, attrs);<br \/>\n\t}<\/p>\n<p>\tprivate void initPreference(Context context, AttributeSet attrs) {<br \/>\n\t\tsetValuesFromXml(attrs);<br \/>\n\t\tmSeekBar = new SeekBar(context, attrs);<br \/>\n\t\tmSeekBar.setMax(mMaxValue &#8211; mMinValue);<br \/>\n\t\tmSeekBar.setOnSeekBarChangeListener(this);<\/p>\n<p>\t\tsetWidgetLayoutResource(R.layout.seek_bar_preference);<br \/>\n\t}<\/p>\n<p>\tprivate void setValuesFromXml(AttributeSet attrs) {<br \/>\n\t\tmMaxValue = attrs.getAttributeIntValue(ANDROIDNS, &#8220;max&#8221;, 100);<br \/>\n\t\tmMinValue = attrs.getAttributeIntValue(APPLICATIONNS, &#8220;min&#8221;, 0);<\/p>\n<p>\t\tmUnitsLeft = getAttributeStringValue(attrs, APPLICATIONNS, &#8220;unitsLeft&#8221;, &#8220;&#8221;);<br \/>\n\t\tString units = getAttributeStringValue(attrs, APPLICATIONNS, &#8220;units&#8221;, &#8220;&#8221;);<br \/>\n\t\tmUnitsRight = getAttributeStringValue(attrs, APPLICATIONNS, &#8220;unitsRight&#8221;, units);<\/p>\n<p>\t\ttry {<br \/>\n\t\t\tString newInterval = attrs.getAttributeValue(APPLICATIONNS, &#8220;interval&#8221;);<br \/>\n\t\t\tif(newInterval != null)<br \/>\n\t\t\t\tmInterval = Integer.parseInt(newInterval);<br \/>\n\t\t}<br \/>\n\t\tcatch(Exception e) {<br \/>\n\t\t\tLog.e(TAG, &#8220;Invalid interval value&#8221;, e);<br \/>\n\t\t}<\/p>\n<p>\t}<\/p>\n<p>\tprivate String getAttributeStringValue(AttributeSet attrs, String namespace, String name, String defaultValue) {<br \/>\n\t\tString value = attrs.getAttributeValue(namespace, name);<br \/>\n\t\tif(value == null)<br \/>\n\t\t\tvalue = defaultValue;<\/p>\n<p>\t\treturn value;<br \/>\n\t}<\/p>\n<p>\t@Override<br \/>\n\tprotected View onCreateView(ViewGroup parent) {<br \/>\n\t\tView view = super.onCreateView(parent);<\/p>\n<p>\t\t\/\/ The basic preference layout puts the widget frame to the right of the title and summary,<br \/>\n\t\t\/\/ so we need to change it a bit &#8211; the seekbar should be under them.<br \/>\n\t\tLinearLayout layout = (LinearLayout) view;<br \/>\n\t\tlayout.setOrientation(LinearLayout.VERTICAL);<\/p>\n<p>\t\treturn view;<br \/>\n\t}<\/p>\n<p>\t@Override<br \/>\n\tpublic void onBindView(View view) {<br \/>\n\t\tsuper.onBindView(view);<\/p>\n<p>\t\ttry {<br \/>\n\t\t\t\/\/ move our seekbar to the new view we&#8217;ve been given<br \/>\n\t\t\tViewParent oldContainer = mSeekBar.getParent();<br \/>\n\t\t\tViewGroup newContainer = (ViewGroup) view.findViewById(R.id.seekBarPrefBarContainer);<\/p>\n<p>\t\t\tif (oldContainer != newContainer) {<br \/>\n\t\t\t\t\/\/ remove the seekbar from the old view<br \/>\n\t\t\t\tif (oldContainer != null) {<br \/>\n\t\t\t\t\t((ViewGroup) oldContainer).removeView(mSeekBar);<br \/>\n\t\t\t\t}<br \/>\n\t\t\t\t\/\/ remove the existing seekbar (there may not be one) and add ours<br \/>\n\t\t\t\tnewContainer.removeAllViews();<br \/>\n\t\t\t\tnewContainer.addView(mSeekBar, ViewGroup.LayoutParams.FILL_PARENT,<br \/>\n\t\t\t\tViewGroup.LayoutParams.WRAP_CONTENT);<br \/>\n\t\t\t}<br \/>\n\t\t}<br \/>\n\t\tcatch(Exception ex) {<br \/>\n\t\t\tLog.e(TAG, &#8220;Error binding view: &#8221; + ex.toString());<br \/>\n\t\t}<\/p>\n<p>\t\t\/\/if dependency is false from the beginning, disable the seek bar<br \/>\n\t\tif (view != null &#038;&#038; !view.isEnabled())<br \/>\n\t\t{<br \/>\n\t\t\tmSeekBar.setEnabled(false);<br \/>\n\t\t}<\/p>\n<p>\t\tupdateView(view);<br \/>\n\t}<\/p>\n<p>    \t\/**<br \/>\n\t * Update a SeekBarPreference view with our current state<br \/>\n\t * @param view<br \/>\n\t *\/<br \/>\n\tprotected void updateView(View view) {<\/p>\n<p>\t\ttry {<br \/>\n\t\t\tmStatusText = (TextView) view.findViewById(R.id.seekBarPrefValue);<\/p>\n<p>\t\t\tmStatusText.setText(String.valueOf(mCurrentValue));<br \/>\n\t\t\tmStatusText.setMinimumWidth(30);<\/p>\n<p>\t\t\tmSeekBar.setProgress(mCurrentValue &#8211; mMinValue);<\/p>\n<p>\t\t\tTextView unitsRight = (TextView)view.findViewById(R.id.seekBarPrefUnitsRight);<br \/>\n\t\t\tunitsRight.setText(mUnitsRight);<\/p>\n<p>\t\t\tTextView unitsLeft = (TextView)view.findViewById(R.id.seekBarPrefUnitsLeft);<br \/>\n\t\t\tunitsLeft.setText(mUnitsLeft);<\/p>\n<p>\t\t}<br \/>\n\t\tcatch(Exception e) {<br \/>\n\t\t\tLog.e(TAG, &#8220;Error updating seek bar preference&#8221;, e);<br \/>\n\t\t}<\/p>\n<p>\t}<\/p>\n<p>\t@Override<br \/>\n\tpublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {<br \/>\n\t\tint newValue = progress + mMinValue;<\/p>\n<p>\t\tif(newValue > mMaxValue)<br \/>\n\t\t\tnewValue = mMaxValue;<br \/>\n\t\telse if(newValue < mMinValue)\n\t\t\tnewValue = mMinValue;\n\t\telse if(mInterval != 1 &#038;&#038; newValue % mInterval != 0)\n\t\t\tnewValue = Math.round(((float)newValue)\/mInterval)*mInterval;  \n\t\t\n\t\t\/\/ change rejected, revert to the previous value\n\t\tif(!callChangeListener(newValue)){\n\t\t\tseekBar.setProgress(mCurrentValue - mMinValue); \n\t\t\treturn; \n\t\t}\n\n\t\t\/\/ change accepted, store it\n\t\tmCurrentValue = newValue;\n\t\tmStatusText.setText(String.valueOf(newValue));\n\t\tpersistInt(newValue);\n\n\t}\n\n\t@Override\n\tpublic void onStartTrackingTouch(SeekBar seekBar) {}\n\n\t@Override\n\tpublic void onStopTrackingTouch(SeekBar seekBar) {\n\t\tnotifyChanged();\n\t}\n\n\n\t@Override \n\tprotected Object onGetDefaultValue(TypedArray ta, int index){\n\t\t\n\t\tint defaultValue = ta.getInt(index, DEFAULT_VALUE);\n\t\treturn defaultValue;\n\t\t\n\t}\n\n\t@Override\n\tprotected void onSetInitialValue(boolean restoreValue, Object defaultValue) {\n\n\t\tif(restoreValue) {\n\t\t\tmCurrentValue = getPersistedInt(mCurrentValue);\n\t\t}\n\t\telse {\n\t\t\tint temp = 0;\n\t\t\ttry {\n\t\t\t\ttemp = (Integer)defaultValue;\n\t\t\t}\n\t\t\tcatch(Exception ex) {\n\t\t\t\tLog.e(TAG, \"Invalid default value: \" + defaultValue.toString());\n\t\t\t}\n\t\t\t\n\t\t\tpersistInt(temp);\n\t\t\tmCurrentValue = temp;\n\t\t}\n\t\t\n\t}\n\t\n\t\/**\n\t* make sure that the seekbar is disabled if the preference is disabled\n\t*\/\n\t@Override\n\tpublic void setEnabled(boolean enabled) {\n\t\tsuper.setEnabled(enabled);\n\t\tmSeekBar.setEnabled(enabled);\n\t}\n\t\n\t@Override\n\tpublic void onDependencyChanged(Preference dependency, boolean disableDependent) {\n\t\tsuper.onDependencyChanged(dependency, disableDependent);\n\t\t\n\t\t\/\/Disable movement of seek bar when dependency is false\n\t\tif (mSeekBar != null)\n\t\t{\n\t\t\tmSeekBar.setEnabled(!disableDependent);\n\t\t}\n\t}\n}\n[\/cc]\n\nThe preference layout file. You can use this to tweek what the preference looks like. You should put this in your res\/layout directory.\n<a href=\"\/blog_files\/android_seekbar_preference\/seek_bar_preference.xml\">seek_bar_preference.xml<\/a><br \/>\n[cc lang=&#8221;xml&#8221;]<br \/>\n<?xml version=\"1.0\" encoding=\"UTF-8\"?><br \/>\n<RelativeLayout xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n    android:id=\"@android:id\/widget_frame\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingBottom=\"5dp\"\n    android:paddingLeft=\"15dp\"\n    android:paddingRight=\"10dp\"\n    android:paddingTop=\"5dp\" ><\/p>\n<p>    <TextView\n        android:id=\"@+id\/seekBarPrefUnitsRight\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_alignParentTop=\"true\" ><br \/>\n    <\/TextView><\/p>\n<p>    <TextView\n        android:id=\"@+id\/seekBarPrefValue\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_toLeftOf=\"@id\/seekBarPrefUnitsRight\"\n        android:gravity=\"right\" ><br \/>\n    <\/TextView><\/p>\n<p>    <TextView\n        android:id=\"@+id\/seekBarPrefUnitsLeft\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_toLeftOf=\"@id\/seekBarPrefValue\" ><br \/>\n    <\/TextView><\/p>\n<p>    <LinearLayout\n        android:id=\"@+id\/seekBarPrefBarContainer\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_below=\"@+id\/seekBarPrefUnitsRight\"\n        android:orientation=\"horizontal\" ><\/p>\n<p>        <SeekBar\n            android:id=\"@+id\/seekBarPrefSeekBar\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\" \/><br \/>\n    <\/LinearLayout><\/p>\n<p><\/RelativeLayout><br \/>\n[\/cc]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A couple of years ago, I posted an Android SeekBar preference widget that I&#8217;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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[2],"tags":[],"_links":{"self":[{"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=\/wp\/v2\/posts\/190"}],"collection":[{"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=190"}],"version-history":[{"count":0,"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=\/wp\/v2\/posts\/190\/revisions"}],"wp:attachment":[{"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/robobunny.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}