Tuesday, November 29, 2011

Android Preferences


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());
       
    }
   
}