Android – Customizing the layout of a PreferenceScreen

androidcustomizinglayoutpreferencepreferencescreen

My requirements

Note: I need to support Android API 15 and onwards.

In my PreferenceFragment I am dynamically adding PreferenceScreen's to a PreferenceCategory.

PreferenceScreen as = mgr.createPreferenceScreen(context);
as.setTitle(name); 
myCategory.addPreference(as);

When the settings Activity renders these PreferenceScreens are rendered with just a title in it's row, see the image below.
PreferenceScreen shown in a settings activity

I want to customize what is shown in this row. Ideally I would like to have a title with a summary below, an icon to the left of the title/summary and on certain rows an icon on the right hand side. See the mockup image below.

Desired rendering of my Preference Screen

PreferenceScreen.setWidgetLayoutResource(int widgetLayoutResId)

I know I can use "setWidgetLayoutResource" to add an icon to the right of the row layout (and a summary with "setSummary") using the following code in my settings activity

PreferenceScreen as = mgr.createPreferenceScreen(context);
as.setTitle(name); 
as.setSummary(summary);
as.setWidgetLayoutResource(R.layout.as_widget);
myCategory.addPreference(as);

where "as_widget.xml" is

<?xml version="1.0" encoding="utf-8"?>
<ImageView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_action_favorite"/>

this will produce UI like the following
Layout using setWidgetLayoutResource

This gets me closer to what I would like but still not exactly what I would like (missing the icon at the beginning of the row).

PreferenceScreen.setLayoutResource(int layoutResId)

I believe if I use this, then I can control the rendering of the whole row. I have seen examples of this on stackoverflow, such as

From my understanding the layout file you specify has to have the following

  • its root View with the id "@android:id/widget_frame"
  • a view with the id android:id="@+android:id/title" (this is where the
    string specified in PreferenceScreen.setTitle is placed)
  • a view with the id android:id="@+android:id/summary" (this is where
    the string specified in PreferenceScreen.setSummary is placed)

However when I try and do this, Android Studio highlights "@+android:id/summary" with the error "Can not resolve symbol '@+android:id/summary'. When the application runs my rows are rendered completely blank.

My java is

PreferenceScreen as = mgr.createPreferenceScreen(context);
as.setTitle(name); 
as.setSummary(summary);
as.setLayoutResource(R.layout.as_row_layout);
myCategory.addPreference(as);

And my layout is

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/widget_frame"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:gravity="center_vertical"
    android:paddingRight="?android:attr/scrollbarSize">

    <ImageView
        android:id="@+id/icon1"
        android:src="@drawable/ic_action_email"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dip"
        android:layout_marginRight="6dip"
        android:layout_marginTop="6dip"
        android:layout_marginBottom="6dip"
        android:layout_weight="1">

        <TextView android:id="@+android:id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:ellipsize="marquee"
            android:fadingEdge="horizontal" />

        <TextView android:id="@+android:id/summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@android:id/title"
            android:layout_alignLeft="@android:id/title"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:textColor="?android:attr/textColorSecondary"
            android:maxLines="4" />

    </RelativeLayout>

    <ImageView
        android:id="@+id/icon2"
        android:src="@drawable/ic_action_favorite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />
</LinearLayout>

Extending PreferenceScreen

I looked at extended PreferenceScreen to overwrite how it renders, but it appears this class has now been made final so all examples on the internet that do it that way can not be used.

Best Solution

I have managed to get this working.

The Preference class uses com.android.internal.R.layout.preference as its layout. This contains an ImageView for an icon on the left hand side, then the title and summary textviews and finally a widget_frame Layout on the right hand side.

By calling "PreferenceScreen.setIcon(..)" you can set the drawable to place in the icon image view. By calling PreferenceScreen.setWidgetLayoutResource("...") you can set the layout to place in the widget_frame layout, in my case I put an ImageView layout containing my image.

Here is my Java code.

PreferenceScreen as = mgr.createPreferenceScreen(context);
as.setTitle(name);
as.setSummary(summary);

// add an icon to the PreferenceScreen, 
// this is rendered on the left hand side of the row
accountScreen.setIcon(R.drawable.my_pref_icon);

// specify the layout to inflate into the widget area on the 
// right hand side of the row, this layout is just my image
as.setWidgetLayoutResource(R.layout.as_widget);

myCategory.addPreference(as);

This produces a layout like the following unaligned icons

The problem with this layout is that the icons do not left align with the text of the preferences below which have no icons.

This can be resolved by specifying the layout for the PreferenceScreen as well. I copied Android's preference.xml into my project (renaming it appropriately for my usecase) and I changing the ImageView to have a left padding and margin of 0dp.

From

<ImageView
    android:id="@+android:id/icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    />

to

<ImageView
    android:id="@+android:id/icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:paddingLeft="0dp"
    android:layout_marginLeft="0dp"/>

I then specified my copy of preference.xml for the PreferenceScreen's layout. So my java is now

PreferenceScreen as = mgr.createPreferenceScreen(context);
as.setTitle(name);
as.setSummary(summary);

// add an icon to the PreferenceScreen, 
// this is rendered on the left hand side of the row
accountScreen.setIcon(R.drawable.my_pref_icon);

// specify the layout to inflate into the widget area on the 
// right hand side of the row, this layout is just my image
as.setWidgetLayoutResource(R.layout.as_widget);

// specify the layout for the preference screen row when it is
// rendered as a row in a preference activity/fragment
as.setLayoutResource(R.layout.preference_row_layout);

myCategory.addPreference(as);

I believe the reason my original attempt at using PreferenceScreen.setLayoutResource was not working was because the layout I specified was incorrect. The incorrect layout had the whole layout with an id of @android:id/widget_frame, i.e.

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

The correct layout does not need an id for the main layout, but needs to contain child views with ids of @+android:id/icon, @+android:id/title, @+android:id/summary, @+android:id/widget_frame, i.e.

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

    <ImageView android:id="@+android:id/icon" ....>
     ....
    <TextView android:id="@+android:id/title" ...>
     ....
    <TextView android:id="@+android:id/summary" ...>
    ....
    <LinearLayout android:id="@+android:id/widget_frame" ..>
    ....
</LinearLayout>
Related Question