Wednesday, May 30, 2012

How do I display the current value of an Android Preference in the Preference summary?


This must come up very often.



When the user is editing preferences in an Android app, I'd like them to be able to see the currently set value of the preference in the Preference summary.



Example: if I have a Preference setting for "Discard old messages" that specifies the number of days after which messages need to be cleaned up. In the PreferenceActivity I'd like the user to see:



"Discard old messages" <- title



"Clean up messages after x days" <- summary where x is the current Preference value



Extra credit: make this reusable, so I can easily apply it to all my preferences regardless of their type (so that it work with EditTextPreference, ListPreference etc. with minimal amount of coding).


Source: Tips4all

11 comments:

  1. Unfortunately there doesn't seem to be a simple, automated way of doing this (you'll notice that very few of the 'native' preferences do this). That said, I'm sure it's a pretty common requirement, so here's the technique I use to achieve it.

    The key is to make your PreferenceActivity class implement OnSharedPreferenceChangeListener. Using the onSharedPreferenceChanged method you can listen for specific preference keys and use setSummary on the related preference control to modify the text, like this:

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    // Let's do something a preference value changes
    if (key.equals(KEY_A_CHECKBOX_PREFERENCE)) {
    mCheckBoxPreference.setSummary(sharedPreferences.getBoolean(key, false) ? "Disable this setting" : "Enable this setting");
    }
    else if (key.equals(KEY_AN_EDITTEXT_PREFERENCE)) {
    mEditBoxPreference.setSummary("Current value is " + sharedPreferences.getString(key, ""));
    }
    }


    Listen for changes in preference using the registerOnSharedPreferenceChangeListener method on the applications Shared Preferences to assign this class as a listener, like this:

    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);


    Your best bet is to register it in onResume and deregister on onPause. You'll also need to update the text values when you register the listener to ensure you get the right initial values.

    The following example is based on the AdvancedPreferences example project in the Android code samples.

    public class AdvancedPreferences extends PreferenceActivity implements OnSharedPreferenceChangeListener {
    public static final String KEY_LIST_PREFERENCE = "list_preference";
    public static final String KEY_CHECKBOX_PREFERENCE = "checkbox_preference";

    private CheckBoxPreference mCheckBoxPreference;
    private ListPreference mListPreference;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Load the XML preferences file
    addPreferencesFromResource(R.xml.advanced_preferences);

    // Get a reference to the preferences
    mCheckBoxPreference = (CheckBoxPreference)getPreferenceScreen().findPreference(KEY_ADVANCED_CHECKBOX_PREFERENCE);
    mListPreference = (ListPreference)getPreferenceScreen().findPreference(KEY_LIST_PREFERENCE);
    }

    @Override
    protected void onResume() {
    super.onResume();

    // Setup the initial values
    mCheckBoxPreference.setSummary(sharedPreferences.getBoolean(key, false) ? "Disable this setting" : "Enable this setting");
    mListPreference.setSummary("Current value is " + sharedPreferences.getValue(key, ""));

    // Set up a listener whenever a key changes
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
    super.onPause();

    // Unregister the listener whenever a key changes
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    // Let's do something a preference value changes
    if (key.equals(KEY_CHECKBOX_PREFERENCE)) {
    mCheckBoxPreference.setSummary(sharedPreferences.getBoolean(key, false) ? "Disable this setting" : "Enable this setting");
    }
    else if (key.equals(KEY_LIST_PREFERENCE)) {
    mListPreference.setSummary("Current value is " + sharedPreferences.getValue(key, ""));
    }
    }

    }

    ReplyDelete
  2. There are ways to make this a more generic solution, if that suits your needs.

    For example, if you want to generically have all list preferences show their choice as summary, you could have this for your onSharedPreferenceChanged implementation:

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    Preference pref = findPreference(key);

    if (pref instanceof ListPreference) {
    ListPreference listPref = (ListPreference) pref;
    pref.setSummary(listPref.getEntry());
    }
    }


    This is easily extensible to other preference classes.

    And by using the getPreferenceCount and getPreference functionality in PreferenceScreen and PreferenceCategory, you could easily write a generic function to walk the preference tree setting the summaries of all preferences of the types you desire to their toString representation

    ReplyDelete
  3. Here is my solution... FWIW

    package com.example.PrefTest;

    import android.content.SharedPreferences;
    import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
    import android.os.Bundle;
    import android.preference.EditTextPreference;
    import android.preference.ListPreference;
    import android.preference.Preference;
    import android.preference.PreferenceActivity;
    import android.preference.PreferenceCategory;
    import android.preference.PreferenceManager;

    public class Preferences extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    addPreferencesFromResource(R.xml.preferences);
    PreferenceManager.setDefaultValues(Preferences.this, R.xml.preferences, false);

    for(int i=0;i<getPreferenceScreen().getPreferenceCount();i++){
    initSummary(getPreferenceScreen().getPreference(i));
    }
    }

    @Override
    protected void onResume(){
    super.onResume();
    // Set up a listener whenever a key changes
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
    super.onPause();
    // Unregister the listener whenever a key changes
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    updatePrefSummary(findPreference(key));
    }

    private void initSummary(Preference p){
    if (p instanceof PreferenceCategory){
    PreferenceCategory pCat = (PreferenceCategory)p;
    for(int i=0;i<pCat.getPreferenceCount();i++){
    initSummary(pCat.getPreference(i));
    }
    }else{
    updatePrefSummary(p);
    }

    }

    private void updatePrefSummary(Preference p){
    if (p instanceof ListPreference) {
    ListPreference listPref = (ListPreference) p;
    p.setSummary(listPref.getEntry());
    }
    if (p instanceof EditTextPreference) {
    EditTextPreference editTextPref = (EditTextPreference) p;
    p.setSummary(editTextPref.getText());
    }

    }
    }

    ReplyDelete
  4. This is the code you need to set the summary to the chosen value. It also sets the values on startup and respects the default values, not only on change. Just change "R.layout.prefs" to your xml-file and extend the setSummary-method to your needs. It actually is only handling ListPreferences, but it is easy to customize to respect other Preferences.

    package de.koem.timetunnel;

    import android.content.SharedPreferences;
    import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
    import android.os.Bundle;
    import android.preference.ListPreference;
    import android.preference.Preference;
    import android.preference.PreferenceActivity;
    import android.preference.PreferenceGroup;

    public class Prefs
    extends PreferenceActivity
    implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    this.addPreferencesFromResource(R.layout.prefs);
    this.initSummaries(this.getPreferenceScreen());

    this.getPreferenceScreen().getSharedPreferences()
    .registerOnSharedPreferenceChangeListener(this);
    }

    /**
    * Set the summaries of all preferences
    */
    private void initSummaries(PreferenceGroup pg) {
    for (int i = 0; i < pg.getPreferenceCount(); ++i) {
    Preference p = pg.getPreference(i);
    if (p instanceof PreferenceGroup)
    this.initSummaries((PreferenceGroup) p); // recursion
    else
    this.setSummary(p);
    }
    }

    /**
    * Set the summaries of the given preference
    */
    private void setSummary(Preference pref) {
    // react on type or key
    if (pref instanceof ListPreference) {
    ListPreference listPref = (ListPreference) pref;
    pref.setSummary(listPref.getEntry());
    }
    }

    /**
    * used to change the summary of a preference
    */
    public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
    Preference pref = findPreference(key);
    this.setSummary(pref);
    }

    // private static final String LOGTAG = "Prefs";
    }


    koem

    ReplyDelete
  5. Thanks, Reto, for the detailed explanation!

    In case this is of any help to anyone, I had to change the code proposed by Reto Meier to make it work with the SDK for Android 1.5

    @Override
    protected void onResume() {
    super.onResume();

    // Setup the initial values
    mListPreference.setSummary("Current value is " + mListPreference.getEntry().toString());

    // Set up a listener whenever a key changes
    ...
    }


    The same change applies for the callback function onSharedPreferenceChanged(...)

    Cheers,

    Chris

    ReplyDelete
  6. Actually, CheckBoxPreference does have the ability to specify a different summary based on the checkbox value. See the android:summaryOff and android:summaryOn attributes (as well as the corresponding CheckBoxPreference methods).

    ReplyDelete
  7. Thanks for this tip!

    I have one preference screen and want to show the value for each list preference as the summary.

    This is my way now:

    public class Preferences extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.preferences);
    }

    @Override
    protected void onResume() {
    super.onResume();

    // Set up initial values for all list preferences
    Map<String, ?> sharedPreferencesMap = getPreferenceScreen().getSharedPreferences().getAll();
    Preference pref;
    ListPreference listPref;
    for (Map.Entry<String, ?> entry : sharedPreferencesMap.entrySet()) {
    pref = findPreference(entry.getKey());
    if (pref instanceof ListPreference) {
    listPref = (ListPreference) pref;
    pref.setSummary(listPref.getEntry());
    }
    }

    // Set up a listener whenever a key changes
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
    super.onPause();

    // Unregister the listener whenever a key changes
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    Preference pref = findPreference(key);

    if (pref instanceof ListPreference) {
    ListPreference listPref = (ListPreference) pref;
    pref.setSummary(listPref.getEntry());
    }
    }


    This works for me, but I'm wondering what is the best solution (performance, stability, scalibility): the one Koem is showing or this one?

    ReplyDelete
  8. My option is to extend ListPreference and it's clean:

    public class ListPreferenceShowSummary extends ListPreference {

    private final static String TAG = ListPreferenceShowSummary.class.getName();

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

    public ListPreferenceShowSummary(Context context) {
    super(context);
    init();
    }

    private void init() {
    LogIncoming.e(TAG, "init");

    setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

    @Override
    public boolean onPreferenceChange(Preference arg0, Object arg1) {
    LogIncoming.w(TAG, "display score changed to "+arg1);
    arg0.setSummary(getEntry());
    return true;
    }
    });
    }

    @Override
    public CharSequence getSummary() {
    return super.getEntry();
    }


    }

    Then you add in your settings.xml:

    <yourpackage.ListPreferenceShowSummary
    android:key="key" android:title="title"
    android:entries="@array/entries" android:entryValues="@array/values"
    android:defaultValue="first value"/>

    ReplyDelete
  9. To set the summary of a ListPreference to the value selected in a dialog you could use this code:

    package yourpackage;

    import android.content.Context;
    import android.util.AttributeSet;

    public class ListPreference extends android.preference.ListPreference {

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

    protected void onDialogClosed(boolean positiveResult) {
    super.onDialogClosed(positiveResult);
    if (positiveResult) setSummary(getEntry());
    }

    protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
    super.onSetInitialValue(restoreValue, defaultValue);
    setSummary(getEntry());
    }
    }


    and reference the yourpackage.ListPreference object in your preferences.xml remembering to specify there your android:defaultValue as this triggers the call to onSetInitialValue().

    If you want you can then modify the text before calling setSummary() to whatever suits your application.

    ReplyDelete
  10. Android documentation says one can use a String formatting marker in getSummary():


    If the summary has a String formatting marker in it (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place.


    Simply specifying android:summary="Clean up messages after %s days" in ListPreference xml declaration worked for me.

    ReplyDelete
  11. I've seen all voted answers show how to set the summary with the exact current value, but the asker wanted also something like:


    "Clean up messages after x days"* <- summary where x is the current Preference value


    Here is my answer for achieving that

    As documentation says about ListPreference.getSummary():


    Returns the summary of this ListPreference. If the summary has a String formatting marker in it (i.e. "%s" or "%1$s"), then the current
    entry value will be substituted in its place.


    However, I tried in several devices and it doesn't seem to work. With some research I found a good solution in this answer. It simply consists of extend every Preference you use and override getSummary() to work as specified by Android documentation.

    ReplyDelete