How to display an icon in the preferences

In the Android preferences you can put different types of controls: check box, edit box, list,…, but none of these displays an icon (see Settings).
Usually a preference consists of two lines, the title and the summary, and after you have clicked you get a dialog box where you can select the chosen item, as you can see in the two pictures below.

font_preference_arrow font_preference_dialog

Note that there are no icons.

In this post I write an example to create a preference where a icon is shown and chosen from the user, you can download the whole project here.

How it works

  1. the layouts: there are four layouts
    • activity_main.xml the layout for the Activity containing the TextView “Hello world!” only
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:paddingBottom="@dimen/activity_vertical_margin"
          android:paddingLeft="@dimen/activity_horizontal_margin"
          android:paddingRight="@dimen/activity_horizontal_margin"
          android:paddingTop="@dimen/activity_vertical_margin"
          tools:context=".MainActivity" >
      
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@string/hello_world" />
      
      </RelativeLayout>
      
    • icon_item_preference.xml the layout for the preferences, in this example it consists of only one item containing a title, a summary and an icon
      <?xml version="1.0" encoding="utf-8"?>
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:paddingBottom="6dp"
          android:paddingLeft="6dp"
          android:paddingRight="6dp"
          android:paddingTop="6dp" >
          
      <!--     android:paddingLeft="72dp" -->
      
          <TextView
              android:id="@+android:id/title"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:textAppearance="?android:attr/textAppearanceMedium" />
      
          <TextView
              android:id="@+android:id/summary"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_alignLeft="@android:id/title"
              android:layout_below="@android:id/title"
              android:textAppearance="?android:attr/textAppearanceSmall"
              android:text="@string/default_summary" />
      
          <ImageView
              android:id="@+id/iconSelected"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_alignParentRight="true" />
      
      </RelativeLayout>
      

      the padding values of the RelativeLayout are for the version 4.x of Android, for earlier versions the paddingLeft should be set to 72dp

    • item_picker.xml the layout that handles the selection of the icon from the user, it contains the summary (the name of the icon), the icon and a radio button
      <?xml version="1.0" encoding="utf-8"?>
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:padding="10dp" >
      
          <TextView
              android:id="@+id/iconName"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_alignParentLeft="true"
              android:layout_alignParentTop="true" />
      
          <RadioButton
              android:id="@+id/iconRadio"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_alignParentRight="true"
              android:layout_below="@+id/iconName"
              android:focusable="false"
              android:clickable="false" />
      
          <ImageView
              android:id="@+id/iconImage"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_alignLeft="@+id/iconName"
              android:layout_below="@+id/iconName" />
      
      </RelativeLayout>
      
    • preferences.xml this layout is not in the directory “res/layout” unlike previous ones but in the directory “res/xml”, the class IconPickerPreference is bound to this file which has the file icon_item_preference.xml (line 10) as layout
      <?xml version="1.0" encoding="utf-8"?>
      <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:custom="http://schemas.android.com/apk/res/eu.lucazanini.custompreference" >
      
          <PreferenceCategory android:title="Icon Picker" >
              <eu.lucazanini.custompreference.IconPickerPreference
                  android:entries="@array/iconName"
                  android:entryValues="@array/iconFile"
                  android:key="@string/custom_icon_key"
                  android:layout="@layout/icon_item_preference"
                  android:title="@string/icon_label"
                  android:summary="Select an icon"
                  custom:iconFile="@string/icon_default" />
          </PreferenceCategory>
      
      </PreferenceScreen>
      
  2. the files in the directory “values”:
    • strings.xml contains the strings used by the app and among these:
      • icon_default the file without extension of the default icon
      • custom_icon_key the key of the selected icon
      • icon_label the title
      • default_summary the initial value of the summary
      • iconName strings array with the names of the icons
      • iconFile strings array with the names of the files without extension of the icons, one of these is equal to custom_icon_key
      <?xml version="1.0" encoding="utf-8"?>
      <resources>
      
          <string name="app_name">Custom Preference</string>
          <string name="action_settings">Settings</string>
          <string name="hello_world">Hello world!</string>
          
          <string name="icon_default">ic_green</string>
          
          <string name="custom_icon_key">customIconKey</string>
          <string name="icon_label">Icon</string>
          
          <string name="default_summary">Select an icon</string>
          
              <string-array name="iconName">
              <item>Green</item>
              <item>Yellow</item>
              <item>Red</item>
          </string-array>
      
          <string-array name="iconFile">
              <item>ic_green</item>
              <item>ic_yellow</item>
              <item>ic_red</item>
          </string-array>
      
      </resources>
      
    • attrs_icon.xml defines the custom attribute iconFile used in preferences.xml at the line 13
      <?xml version="1.0" encoding="utf-8"?>
      <resources>
         <declare-styleable name="attrs_icon">
             <attr name="iconFile" format="string" />
         </declare-styleable>
      </resources>
      
  3. the java classes:
    • MainActivity.java the starting activity, it is only to launch the preferences, lines 27-30
      package eu.lucazanini.custompreference;
      
      import android.app.Activity;
      import android.content.Intent;
      import android.os.Bundle;
      import android.view.Menu;
      import android.view.MenuItem;
      
      public class MainActivity extends Activity {
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
      	super.onCreate(savedInstanceState);
      	setContentView(R.layout.activity_main);
          }
      
          @Override
          public boolean onCreateOptionsMenu(Menu menu) {
      	// Inflate the menu; this adds items to the action bar if it is present.
      	getMenuInflater().inflate(R.menu.main, menu);
      	return true;
          }
      
          @Override
          public boolean onOptionsItemSelected(MenuItem item) {
      	switch (item.getItemId()) {
      	case R.id.action_settings:
      	    Intent intent = new Intent(this, PreferencesActivity.class);
      	    startActivity(intent);
      	    return true;
      	default:
      	    return super.onOptionsItemSelected(item);
      	}
          }
      
      }
      
    • PreferencesActivity.java the activity of the preferences, it contains an instance of IconPickerFragment
      package eu.lucazanini.custompreference;
      
      import android.app.Activity;
      import android.app.Fragment;
      import android.app.FragmentManager;
      import android.app.FragmentTransaction;
      import android.os.Bundle;
      
      public class PreferencesActivity extends Activity {
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
      	super.onCreate(savedInstanceState);
      
      	Fragment iconPickerFragment = new IconPickerFragment();
      
      	FragmentManager fm = getFragmentManager();
      	FragmentTransaction ft = fm.beginTransaction();
      	ft.replace(android.R.id.content, iconPickerFragment);
      	ft.commit();
      
          }
      
      }
      
    • IconPickerFragment.java, it has the file preferences.xml as layout
      package eu.lucazanini.custompreference;
      
      import android.os.Bundle;
      import android.preference.PreferenceFragment;
      
      public class IconPickerFragment extends PreferenceFragment {
      
          @Override
          public void onCreate(Bundle savedInstanceState) {
      	super.onCreate(savedInstanceState);
      
      	addPreferencesFromResource(R.xml.preferences);
      
          }
      
      }
      
    • IconPickerPreference.java, the key class of the app, it handles the preference set in preferences.xml and the dialog box where the user chooses the icon
      • line 130: the constructor IconPickerPreference initializes context, resources, preferences and defaultIconFile defined in preferences.xml
      • line 155: the method onBindView displays the icon in the preferences
      • line 197: the method onPrepareDialogBuilder is called before the opening of the dialog box, it defines the adapter for the bulder (dialog box)
      • line 170: the method onDialogClosed is called after the dialog box is closed and it saves the choice of the user in the preferences
      • line 27: the class CustomListPreferenceAdapter handles the dialog box
      package eu.lucazanini.custompreference;
      
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
      
      import android.app.AlertDialog.Builder;
      import android.content.Context;
      import android.content.SharedPreferences;
      import android.content.SharedPreferences.Editor;
      import android.content.res.Resources;
      import android.content.res.TypedArray;
      import android.preference.ListPreference;
      import android.preference.PreferenceManager;
      import android.util.AttributeSet;
      import android.view.LayoutInflater;
      import android.view.View;
      import android.view.View.OnClickListener;
      import android.view.ViewGroup;
      import android.widget.ArrayAdapter;
      import android.widget.ImageView;
      import android.widget.RadioButton;
      import android.widget.TextView;
      
      public class IconPickerPreference extends ListPreference {
      
          private class CustomListPreferenceAdapter extends ArrayAdapter<IconItem> {
      
      	private Context context;
      	private List<IconItem> icons;
      	private int resource;
      
      	public CustomListPreferenceAdapter(Context context, int resource,
      		List<IconItem> objects) {
      	    super(context, resource, objects);
      	    this.context = context;
      	    this.resource = resource;
      	    this.icons = objects;
      	}
      
      	@Override
      	public View getView(final int position, View convertView, ViewGroup parent) {
      
      	    ViewHolder holder;
      
      	    if (convertView == null) {
      		LayoutInflater inflater = (LayoutInflater) context
      			.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      		convertView = inflater.inflate(resource, parent, false);
      
      		holder = new ViewHolder();
      		holder.iconName = (TextView) convertView
      			.findViewById(R.id.iconName);
      		holder.iconImage = (ImageView) convertView
      			.findViewById(R.id.iconImage);
      		holder.radioButton = (RadioButton) convertView
      			.findViewById(R.id.iconRadio);
      
      		convertView.setTag(holder);
      	    } else {
      		holder = (ViewHolder) convertView.getTag();
      	    }
      
      	    holder.iconName.setText(icons.get(position).name);
      
      	    int identifier = context.getResources().getIdentifier(
      		    icons.get(position).file, "drawable",
      		    context.getPackageName());
      	    holder.iconImage.setImageResource(identifier);
      
      	    holder.radioButton.setChecked(icons.get(position).isChecked);
      
      	    convertView.setOnClickListener(new OnClickListener() {
      
      		@Override
      		public void onClick(View v) {
      		    ViewHolder holder = (ViewHolder) v.getTag();
      		    for (int i = 0; i < icons.size(); i++) {
      			if (i == position)
      			    icons.get(i).isChecked = true;
      			else
      			    icons.get(i).isChecked = false;
      		    }
      		    getDialog().dismiss();
      		}
      	    });
      
      	    return convertView;
      	}
      
          }
      
          private static class IconItem {
      
      	private String file;
      	private boolean isChecked;
      	private String name;
      
      	public IconItem(CharSequence name, CharSequence file, boolean isChecked) {
      	    this(name.toString(), file.toString(), isChecked);
      	}
      
      	public IconItem(String name, String file, boolean isChecked) {
      	    this.name = name;
      	    this.file = file;
      	    this.isChecked = isChecked;
      	}
      
          }
      
          private static class ViewHolder {
      	protected ImageView iconImage;
      	protected TextView iconName;
      	protected RadioButton radioButton;
          }
      
          private Context context;
          private ImageView icon;
      
          private CharSequence[] iconFile;
          private CharSequence[] iconName;
          private List<IconItem> icons;
          private SharedPreferences preferences;
          private Resources resources;
          private String selectedIconFile, defaultIconFile;
          private TextView summary;
      
          public IconPickerPreference(Context context, AttributeSet attrs) {
      	super(context, attrs);
      	this.context = context;
      
      	resources = context.getResources();
      	preferences = PreferenceManager.getDefaultSharedPreferences(context);
      
      	TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
      		R.styleable.attrs_icon, 0, 0);
      
      	try {
      	    defaultIconFile = a.getString(R.styleable.attrs_icon_iconFile);
      	} finally {
      	    a.recycle();
      	}
          }
      
          private String getEntry(String value) {
      	String[] entries = resources.getStringArray(R.array.iconName);
      	String[] values = resources.getStringArray(R.array.iconFile);
      	int index = Arrays.asList(values).indexOf(value);
      	return entries[index];
          }
      
          @Override
          protected void onBindView(View view) {
      	super.onBindView(view);
      
      	selectedIconFile = preferences.getString(
      		resources.getString(R.string.custom_icon_key), defaultIconFile);
      
      	icon = (ImageView) view.findViewById(R.id.iconSelected);
      	updateIcon();
      
      	summary = (TextView) view.findViewById(android.R.id.summary);
      	summary.setText(getEntry(selectedIconFile));
      
          }
      
          @Override
          protected void onDialogClosed(boolean positiveResult) {
      	super.onDialogClosed(positiveResult);
      
      	if (icons != null) {
      	    for (int i = 0; i < iconName.length; i++) {
      		IconItem item = icons.get(i);
      		if (item.isChecked) {
      
      		    Editor editor = preferences.edit();
      		    editor.putString(
      			    resources.getString(R.string.custom_icon_key),
      			    item.file);
      		    editor.commit();
      
      		    selectedIconFile = item.file;
      		    updateIcon();
      
      		    summary.setText(item.name);
      
      		    break;
      		}
      	    }
      	}
      
          }
      
          @Override
          protected void onPrepareDialogBuilder(Builder builder) {
      
      	builder.setNegativeButton("Cancel", null);
      	builder.setPositiveButton(null, null);
      
      	iconName = getEntries();
      	iconFile = getEntryValues();
      
      	if (iconName == null || iconFile == null
      		|| iconName.length != iconFile.length) {
      	    throw new IllegalStateException(
      		    "ListPreference requires an entries array "
      			    + "and an entryValues array which are both the same length");
      	}
      
      	String selectedIcon = preferences.getString(
      		resources.getString(R.string.custom_icon_key),
      		resources.getString(R.string.icon_default));
      
      	icons = new ArrayList<IconItem>();
      
      	for (int i = 0; i < iconName.length; i++) {
      	    boolean isSelected = selectedIcon.equals(iconFile[i]) ? true
      		    : false;
      	    IconItem item = new IconItem(iconName[i], iconFile[i], isSelected);
      	    icons.add(item);
      	}
      
      	CustomListPreferenceAdapter customListPreferenceAdapter = new CustomListPreferenceAdapter(
      		context, R.layout.item_picker, icons);
      	builder.setAdapter(customListPreferenceAdapter, null);
      
          }
      
          private void updateIcon() {
      	int identifier = resources.getIdentifier(selectedIconFile, "drawable",
      		context.getPackageName());
      
      	icon.setImageResource(identifier);
      	icon.setTag(selectedIconFile);
          }
      
      }
      

The following pictures show the app:

    • MainActivity.java
      custom_preference_settings
    • PreferencesActivity.java and IconPickerFragment.java
      custom_preference_green
  • IconPickerPreference.java
    custom_preference_picker

Comments

10 responses to “How to display an icon in the preferences”

  1. Eyvind Almqvist Avatar
    Eyvind Almqvist

    I tried JJ974s solution on StackOverFlow, but it does not work for several reasons:
    1) Cannot resolve symbol preference_list_icon_picker
    2) Cannot resolve methos setAdapter
    3) No imports included in sample code, which makes it difficult to know which version of Builder class is used.

    Another big problem is that Luca Zaninis original project contains lots of deprecated classes. So it is very risky to use a solution like this in production. It will probably cause lots of crashes.

    Please help me find a modern way to add icons to ListPreference! This would make apps so much more user friendly and more fun to use.

  2. Hi Luca,
    Thank you very much for your nice Listpreference with icons.
    Can I make a minor suggestion?
    I saw that the code has some depedencies beyond the listpreference (own) properties. So, re-use of the solution as-as is not easily possible. Therefore I changed the code a bit so your wonderful solution can be re-used as often as I like. Please refer to http://stackoverflow.com/a/41806449/3143823. At the forums I thanked you for the solution.
    Kind regards,
    Johan, NL

    1. Thank you very much for the reference and I appreciate your improvement too

  3. […] creating a custom ListPreference on the back of this […]

  4. Why can I only use up to 5 elements in the iconPicker?????

    1. As told by juanhl in Android Custom ListPreference only allow 5 items I corrected my code.
      Thanks juanhl and Ronan

  5. Nice…Works perfect 🙂

  6. Gives me this error: please help me plsss…!!!

    2918-2918/com.example.virenjaru.myapplication1 E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.example.virenjaru.myapplication1, PID: 2918
    java.lang.NullPointerException: Attempt to invoke virtual method ‘void android.widget.ImageView.setImageResource(int)’ on a null object reference
    at com.example.virenjaru.myapplication1.IconPickerPreference.updateIcon(IconPickerPreference.java:234)
    at com.example.virenjaru.myapplication1.IconPickerPreference.onBindView(IconPickerPreference.java:161)
    at android.preference.Preference.getView(Preference.java:489)
    at android.preference.PreferenceGroupAdapter.getView(PreferenceGroupAdapter.java:246)
    at android.widget.AbsListView.obtainView(AbsListView.java:2344)
    at android.widget.ListView.makeAndAddView(ListView.java:1864)
    at android.widget.ListView.fillDown(ListView.java:698)
    at android.widget.ListView.fillFromTop(ListView.java:759)
    at android.widget.ListView.layoutChildren(ListView.java:1673)
    at android.widget.AbsListView.onLayout(AbsListView.java:2148)
    at android.view.View.layout(View.java:15596)
    at android.view.ViewGroup.layout(ViewGroup.java:4966)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1703)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1557)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1466)
    at android.view.View.layout(View.java:15596)
    at android.view.ViewGroup.layout(ViewGroup.java:4966)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:573)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:508)

    1. Verify
      icon = (ImageView) view.findViewById(R.id.iconSelected);
      or something like that in your code

  7. Hi I am new to java and am following this guide. I have it working where it shows the images etc but wonder if there is a way to add a third array which contains a value to be set to the settings database? Any help on this would be appreciated.

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.