Search This Blog

Wednesday, January 6, 2010

Android Preferences

For almost any application we need to provide some settings in order to enable users have some level of control over how the application works. Android has provided a standard mechanism to show, save and manipulate user's preferences. PreferenceActivity class is the standard Android Activity to show Preferences page, it contains a bunch of Preference class instances which use SharedPreference
class to save and manipulate corresponding data.There are different types of Preferences Available in Preference package, and if you need something more you can extend Preference class and create your own Preference type. in this article we will go through predefined Preference types in Android and I'm also going to implement a custom Preference type to see how that works.
Our Preferences page is gonna look like this :



as you can see we have two section in our preferences page , "First Category" which contains two options and "Second Category" which contains three options.
here is our Activity which produces this page :



public class MyPreferenceActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

addPreferencesFromResource(R.xml.preferences);
}

}


pretty simple, isn't it? actually it's supposed to be simple and easy thanks to PreferenceActivity which takes care of pretty much anything. as i said PreferenceActivity renders instances of Preference class which in this example
have defined in preferences.xml file under res/xml directory:



<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">

<PreferenceCategory android:title="First Category">
<CheckBoxPreference
android:key="Main_Option"
android:title="Main Option"
android:defaultValue="true"
android:summary="SUMMARY_Main_Option" />


<ListPreference
android:title="List Preference"
android:summary="This preference allows to select an item in an array"
android:dependency="Main_Option"
android:key="listPref"
android:defaultValue="1"
android:entries="@array/colors"
android:entryValues="@array/colors_values" />


</PreferenceCategory>

<PreferenceCategory android:title="Second Category">

<PreferenceScreen android:title="Advanced Options">

<CheckBoxPreference
android:key="Advanced_Option"
android:title="Advanced Option"
android:defaultValue="true"
android:summary="SUMMARY_Advanced_Option"/>

</PreferenceScreen>


<amir.android.icebreaking.SeekBarPreference
android:dependency="Main_Option"
android:key="customPref"
android:defaultValue="32"
android:title="Custom Preference" />

<EditTextPreference android:dialogTitle="EditTextTitle"
android:dialogMessage="EditTextMessage"
android:dependency="Main_Option"
android:key="pref_dialog"
android:title="SomeTitle"
android:summary="Summary"
android:defaultValue="test"/>



</PreferenceCategory>

</PreferenceScreen>



First thing i wanna talk about is dependency, Dependency helps you to disable/enable some Preferences based on the status of another preference. in this example we have three preferences dependent on the first preference so if we disable the first option all dependent options will become uneditable.



if we select the second option we will see something like this :



ListPreference tag has two attribute which is used to populate its content; android:entries which refers to an array of labels and android:entryValues which is also an array that represent the actual value of entries, this value will be saved when each entry gets selected. as you can see I've used "@array/" indicator for these two attribute, it means we have our data in arrays.xml file under res/values directory and here is its content :



<?xml version="1.0" encoding="utf-8"?>


<resources>

<string-array name="colors">
<item>red</item>
<item>orange</item>
<item>yellow</item>
<item>green</item>
<item>blue</item>
<item>violet</item>
</string-array>

<string-array name="colors_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
</string-array>


</resources>



you sometimes want to show a set of Preferences in a different window, to do so you just need to wrap all those Preferences in a PreferenceScreen tag, just like what I've done for "Advanced Options" in this example so when user presses this option we will see something like this :



The last Predefined Preference type I'm gonna talk about is EditTextPreference, when user presses one of these type of Preferences, a dialog with a Text Field pops up and let the user enter a text. I used a EditTextPreference for the last option in this example and you can see here how it's gonna look like after being pressed :



So far we've seen what functionality we have already got in our hand which would fulfill our needs in most circumstances but what if we need something that is not already there? Not a big deal... we'll just need to roll up our sleeves and implement our own Preference type.
I've developed a simple custom preference type which shows a seekbar and stores an Integer value each time user moves the seekbar. you can see how it looks like in the first picture above.
here is our SeekBarPreference class source code :



public class SeekBarPreference extends Preference
implements OnSeekBarChangeListener{


public static int maximum = 100;
public static int interval = 5;

private float oldValue = 50;
private TextView monitorBox;


public SeekBarPreference(Context context) {
super(context);
}

public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}

public SeekBarPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

@Override
protected View onCreateView(ViewGroup parent){

LinearLayout layout = new LinearLayout(getContext());

LinearLayout.LayoutParams params1 = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params1.gravity = Gravity.LEFT;
params1.weight = 1.0f;


LinearLayout.LayoutParams params2 = new LinearLayout.LayoutParams(
80,
LinearLayout.LayoutParams.WRAP_CONTENT);
params2.gravity = Gravity.RIGHT;


LinearLayout.LayoutParams params3 = new LinearLayout.LayoutParams(
30,
LinearLayout.LayoutParams.WRAP_CONTENT);
params3.gravity = Gravity.CENTER;


layout.setPadding(15, 5, 10, 5);
layout.setOrientation(LinearLayout.HORIZONTAL);

TextView view = new TextView(getContext());
view.setText(getTitle());
view.setTextSize(18);
view.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
view.setGravity(Gravity.LEFT);
view.setLayoutParams(params1);


SeekBar bar = new SeekBar(getContext());
bar.setMax(maximum);
bar.setProgress((int)this.oldValue);
bar.setLayoutParams(params2);
bar.setOnSeekBarChangeListener(this);

this.monitorBox = new TextView(getContext());
this.monitorBox.setTextSize(12);
this.monitorBox.setTypeface(Typeface.MONOSPACE, Typeface.ITALIC);
this.monitorBox.setLayoutParams(params3);
this.monitorBox.setPadding(2, 5, 0, 0);
this.monitorBox.setText(bar.getProgress()+"");


layout.addView(view);
layout.addView(bar);
layout.addView(this.monitorBox);
layout.setId(android.R.id.widget_frame);


return layout;
}

@Override
public void onProgressChanged(SeekBar seekBar, int progress,boolean fromUser) {

progress = Math.round(((float)progress)/interval)*interval;

if(!callChangeListener(progress)){
seekBar.setProgress((int)this.oldValue);
return;
}

seekBar.setProgress(progress);
this.oldValue = progress;
this.monitorBox.setText(progress+"");
updatePreference(progress);

notifyChanged();
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}


@Override
protected Object onGetDefaultValue(TypedArray ta,int index){

int dValue = (int)ta.getInt(index,50);

return validateValue(dValue);
}


@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {

int temp = restoreValue ? getPersistedInt(50) : (Integer)defaultValue;

if(!restoreValue)
persistInt(temp);

this.oldValue = temp;
}


private int validateValue(int value){

if(value > maximum)
value = maximum;
else if(value < 0)
value = 0;
else if(value % interval != 0)
value = Math.round(((float)value)/interval)*interval;


return value;
}


private void updatePreference(int newValue){

SharedPreferences.Editor editor = getEditor();
editor.putInt(getKey(), newValue);
editor.commit();
}

}


In all examples I've come across about custom preferences, an XML layout file has been used to create the preference view, so I thought it would be a good idea not to used XML and go programatically and actually it was a good opportunity for a person like me who had always used XML for creating GUIs in Android to go through an alternative way.

anyway onCreateView() method of Preference class is responsible to return a View to be shown by PereferenceActivity,we override this method to make our own custom view, in Preference class documentation we've been asked to return a ViewGroup with "widget_frame" as its ID, that's what I've done and you should do if you want to do something like this.
Another thing to remember is that if you're willing to let clients of this preference type set Listeners and get notified when the status of preference changes you have to call callChangeListener() method before saving new value, this method will invoke the listener callback method and return the result, if client is happy with new value and operation can be carried out the result will be true, otherwise it will be false. Don't forget to call notifyChanged() method once you have saved new value of preference, though I'd forgotten to do so and it was working like a charm ;)

I've also used both Preference class's persistInt() method and Editor class's putInt() method to store data in this example to show different ways that it can be done.

And here is a simple Activity which retrieves preferences values and show them in a textview :



public class ShowSettings extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.show);


SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);

StringBuilder builder = new StringBuilder();

builder.append("\n"+ sp.getBoolean("Main_Option",false));
builder.append("\n"+ sp.getString("listPref","-1"));
builder.append("\n"+ sp.getBoolean("Advanced_Option",false));
builder.append("\n"+ sp.getInt("customPref",-1));
builder.append("\n"+ sp.getString("pref_dialog","NULL"));

TextView view = (TextView)findViewById(R.id.viewBox);
view.setText(builder.toString());

}

}

35 comments:

Russ said...

Very timely, thanks Amir. Was just starting to look at Prefs in my Android learning cycle today.

WazaBe said...

At line 135 of SeekBarPreference.jave i get a Force close:

At the lines:

protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {

int temp = restoreValue ? getPersistedInt(50) : (Integer)defaultValue;



The defaultValue seems to be null!
I declared it in xml file as you said...

Any idea of what's wrong?

(the LogCat is too long to be posted here- I can send by email...)

Amir said...

not sure what it could be, have you overrided onGetDefaultValue() method properly?

Anonymous said...

Thanks a lot Amir. Your code gave me very good help! But when I click seekbar preferece area it doesn't show background color. Do you know how can show background color like general preference property by any chance? I think it's very useful if ther is radiobutton preference like checkbox preference. But after implementing RadioButtonPreference when I clicked preference area it didn't show background color. In real user case I think it would give confusion. I've tried in many ways(ex. over ride getVew(), onBind()..etc..) but all of them couldn't give me expected result. In the end I had to use TextView.background attribute to show backgroud color(It's useful tip :-) In conclusion I think if you implement custom preference class and if inflate layout(I think any layout have same result..) there is no way to show preference background color. So, I am starting to study android original Preference class. If you have any good idea on my comment please let me know..Android application developement is very exciting something! And I have about 3 months experience. Good day~~

Amir said...

I'm so glad it was helpful for you.
actually I have no idea how it can be done , so just let me know if you find out ;)

Enea said...

thank you, very very helpful !

Der Schwartz said...

Have tried to add two of custom preference types that shows a seekbar in your XML. You'll get some funny effects. The controls will swap focus and behave unstable. Any ideas to why this is?

Amir said...

Yes...you are right, the problem is that PreferenceActivity is actually an extension of
ListActivity and by default it reuses old views for updating its children, I didn't know this and I didn't take that into account when I was writing this code.
anyway I think the easiest way would be to somehow change the default behavior so we
will always have just one copy of our view; to do so you're gonna need to override getView()
method like this:

@Override
public View getView(View convertView, ViewGroup parent) {

convertView = this.layout == null ? onCreateView(parent) : this.layout;
return convertView;
}


and since we are not following the default behaviour anymore we need to take care of dependency
issues by ourserlves, it means we will need to override onDependencyChanged() method and disable our GUI components manually :

@Override
public void onDependencyChanged(Preference dependency, boolean disableDependent) {
super.onDependencyChanged(dependency, disableDependent);

///see if it has been initialized
if(this.layout != null){
this.view.setEnabled(!disableDependent);
this.bar.setEnabled(!disableDependent);
this.monitorBox.setEnabled(!disableDependent);
}
}


It might not be the best way to solve this problems but that's what I've come up with and seems it works hopefully without any problem.
The recommended way to sort out this problem though, is overriding onBindView() method, but if I wanted to go down that road, I would probably have to change most of my code which I'm not so willing to do that, besides I'm not sure if that is the best approach in our scenario with a SeekBar which is likely to produce a series of events rather than just a single event like buttons,check-boxes and radio-buttons...

Der Schwartz said...

Great! this made using more than one control stable.

I've added a field for the max value in my preferences.xml:

android:max="300"

And I read this to the control:

private void getPreferenceAttibutes(AttributeSet attrs)
{
maximum = attrs.getAttributeIntValue(androidns,"max", 100);
}

Only problem is that if I add more than one control it os the max value of the last control that will be but to all the controls, this is because you have declared maximum with the static keyword:

public static int maximum = 100;

I removed the static key word and it work like a charm:

public int maximum = 100;

BTW: Great work you're doing here, I've learned a lot on creating custom controls - keep it up :-)

Amir said...

I'm happy you got it working buddy, and thanks for your comment on the 'maximum' attribute, it should definitely be non static....

chrissy said...

i am trying to implement this, but i keep getting an "activitynotfoundexception". i have defined the activity in my manifest file, so i am not sure what else is wrong? thank you for any help!

Amir said...

I'm not sure what it could be, but if you have implemented it just like what has been described in here you shouldn't have any problem, the only thing I can think of is that you might haven't defined you activity properly in your manifest file. if you wanna start this activity as main activity you're gonna need to have something like this :

<activity android:name=".MyPreferenceActivity"
android:label="Preferences">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter >
</activity>

or if it's not the main activity its action and category have to be something else rather than MAIN and LAUNCHER....
you also gotta make sure the intent which is used to start the activity matches the one that has been defined in your <intent-filter> tag inside your manifest file.
last thing that I reckon could be causing this problem is if you have got your Activity in a package other than main package of your project; in that case you're gonna have to use the fully-qualified class name of the activity.

chrissy said...

i'm not sure what the problem was, but i restarted eclipse and it was ok. weird. maybe all the name changing to test getting it to work. anyway, thanks for getting back to me so quickly and thanks for posting this! it was a great help to further understanding this concept.

Shazeb Shamsi said...

Excellent write up. Really cool. Thanks

robbobkirk said...

Great posting. I ended up overriding the text value so I could have non-zero starting points.

Also you definitely need the update on the 6th March to have multiple seek bar preferences.

Anonymous said...

Excellent post. I am looking to do something a little more complex with preferences and I am not sure where to start. I want to be able to have identical preferences for multiple items. As an example, I want the ability to add cars dynamically and for each car have a set of preferences I can define (make, model, color, etc). Is this possible using preferences in this way?

Ken said...

Just wanted to say thanks for a good preferences example :-)

Emil Mork said...

Hey, greate tutorial! Just what i needed for my app :)

Paulo Almeida said...

I there,
First of all tremendous this tutorial, I has looking for a way of doing a custom Preference since long ago.

Just one optimization that you maibe interest.


public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
...
// updatePreference(progress);
// notifyChanged();
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
updatePreference(seekBar.getProgress());
notifyChanged();
}

if you move the saving of the preference to the stop method, it will look must more smother the progressBar since it dosen´t have to be saving the preference all the time.

Unknown said...

Very nice tutorial.
One question though, how would I get this preferences in the "Settings" app of android. What I mean is, how do I make my app settings to be a part of the system settings?

Thanks
Mrinal

Yorel said...

Amir, thank for this great info.

I have 4 PreferenceSeekbar on a preference page and it seems the first 3 are losing Focus as I am dragging the slider. The last one in the set works fine. I tried with just two and it does the samething. The first one loses focus while dragging and the second works fine. Any ideas? Just in case there is an issue with versions I built the App for Android 1.6.

Thanks.

Anonymous said...

Thank you very much. Diego from Buenos Aires Argentina.

Anonymous said...

the onprogresschange method calls notifyChanged()...this makes the seekbar to loose focus. remove it and it'll work like a charm.

thanx

Sebastian Breit

Unknown said...

This is the most useful article about custom preferences on the interwebs. Thank you so much. Two questions. Does the frame _really_ have to be called widget_frame ? Does that mean I cannot include checkboxes in my custom preference design (as widgets don't support checkboxes)?

Anonymous said...

After a lot of searching for preferences, your blog is super.'
Thanks for your work

GattoBlepone said...

For tof162

I think that probably you can solve the problem adding the following attribute in the xml preferences:
android:persistent="true"

Ragupathy said...

hi amir,

i am a new developer to android . after a lot of search for preferences found this article this was very useful to me . Thanks for sharing ideas and this will help others like me .

ragupathy

vijpurpavan73 said...

Awesome man, Preference activity is explained in a great way. Even the discussions give lot of funda.

Pavan V

cchan said...

Thank you. This is very useful. keep it up

Anonymous said...

Thanks.. Great Article! But how do we get the control's layout, font, color, etc. to match with the other default preferences. Like in the article, you are setting padding manually:
layout.setPadding(15, 5, 10, 5);
but this will look odd on different screen resolution.

So how to get Android preferences' default padding, margins, font color, typeface, etc. and apply those to our custom preference control?

mike.b said...

One problem of onProgressChanged: it may be called recursively by seekBar.setProgress(progress) if rounding of progress occured.

Raghavendra said...

Hi Amir,

Your post was really helpful, I was struggling for kind of layout for more than a week. Your post solved my problem.

Roy Barina said...

i've managed to add more than one seekbar preference but when i get to the settings page to change preferences all the "maximum" values set to the last one(I did remove the static declaration in the code) and when i try to change one the others starts messing around!
its been a couple day i was trying to find what i did wrong.. can someone please help me?
thanks a lot

Roy Barina said...

Ok it was because notifyChanges()
that function messing up the whole page
removing it will NOT help because other built-in preferences (such as CheckBoxPreference) notifying changes them self and screwing up everything.. damn.... what the hell i am doing wrong....... :\
the article is very professional and explained well
thank you Amir for your great work and knowledge :)
I'm glad I learned good stuff here
but off course i have a lot to learn more...

Android app developers said...

This is one of the perfect post.I like your blog foundation.I learn some good tips from your post.