Previous  | Next  | Home

Themes, Styles, and Preferences


 

The basic use of styles and themes in Android interfaces has been touched upon in previous projects (for example, in the discussion of Android User Interfaces and Playing Video). A general introduction may be found in the Styles and Themes guide and available themes are documented under R.style. In this project we give a somewhat more extensive discussion, addressing more advanced issues associated with

  1. Creating new themes by combining the attributes of existing ones.

  2. Using SharedPreferences to allow the user to implement a persistent change of the display theme at runtime. (See the Settings document.)

  3. Implementing SharedPreferences within a modern Fragments-based context.

Because we shall use some specific themes and methods that were introduced in more recent versions of Android, we shall restrict this application to devices running API 15 and above.

 

Themes 101

Before writing any code we give a very basic overview of styles and themes. (Styles and themes are basically the same thing, but in the usual terminology styles are applied to individual Views and themes are applied to full Activities.)

 

Simple Applications of Themes

There are many Android attributes and styles and themes. These may be specified in the app manifest file, in XML files, or directly in the Java code.

  1. In the manifest file a theme is specified as an attribute of an activity or application. For example, in the manifest file

    <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.lightcone.niceapp" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="7" /> <application android:theme="@android:style/Theme.Holo.Light" android:icon="@drawable/nice_logo" android:label="@string/app_name" > <activity android:name=".NiceApp" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@android:style/Theme.NoTitleBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".Prefs" android:label="Preferences"> </activity> </application> </manifest>
    the first line in red specifies the Android (Honeycomb) Holo.Light theme for the entire application and the second line in red specifies the Android NoTitleBar theme for the activity corresponding to the class NiceApp. Notice that
  2. A style can be specified in an XML layout file using the style attribute. For example,

    <TextView style="@style/FunkyFont" android:text="@string/hello" />
    (notice that this attribute is not preceded by android:)

  3. A theme can be set using code in the onCreate method of an Activity. For example, the Android theme NoTitleBar can be invoked using the following lines highlighted in red,

    public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // following must precede setContentView(). setTheme(android.R.style.Theme_NoTitleBar); setContentView(R.layout.taskactivity1); }
    where we note explicitly that the theme must be specified using the setTheme(int id) method that Activity inherits from ContextThemeWrapper before setContentView is invoked.

If used individually, Android themes override each other. Often it would be useful to combine multiple themes into a single compound theme (for example, one that has the properties of both Holo.Light and NoTitleBar). Let us now see how to do that.

 

Creating New Themes by Combining Old Ones

We may place our styles and themes definitions in the file res/values/styles.xml, within the tag. For example,


<resources> . . <!-- Use the Android Dialog theme as the base for a new compound theme DialogNoTitle --> <!-- Apply using attribute android:theme="@style/DialogNoTitle in manifest file for activity, --> <!-- or setTheme(R.style.DialogNoTitle) before setContentView() in code. --> <style name="DialogNoTitle" parent="@android:style/Theme.Dialog"> <!-- hide the Window Title --> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@color/bgcolor</item> <item name="android:colorBackground">@color/bgcolor</item> <item name="android:textColor">@color/dialog_brown</item> </style> <style name="DialogNoTitleDark" parent="@android:style/Theme.Dialog"> <!-- hide the Window Title --> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@color/bgcolor2</item> <item name="android:colorBackground">@color/bgcolor</item> <item name="android:textColor">@color/buttontextcolor</item> </style> . . </resources>

The content of each <style> </style> tag defines a new theme, with name the name by which we shall reference it, parent the base theme from which the new theme is derived, and the content of the enclosed <item> </item> tags each adding new attributes to the base theme. For example, the first new custom theme DialogNoTitle adds to the properties of the Android Theme.Dialog theme the attribute windowNoTitle. These user-defined composite themes can then be invoked either in the manifest file or in code, just as for normal themes.


Additional examples of defining custom styles and themes may be found in res/values/styles.xml for the project Progress Bar Example, where we implement some custom styles for ProgressBar widgets. The file res/values/styles.xml will likely already contain style definitions defining the default style of the app. For example,

<resources> . . <!-- Base application theme, dependent on API level. This theme is replaced by AppBaseTheme from res/values-vXX/styles.xml on newer devices. --> <style name="AppBaseTheme" parent="android:Theme.Light"> <!-- Theme customizations available in newer API levels can go in res/values-vXX/styles.xml, while customizations related to backward-compatibility can go here. --> </style> <!-- Application theme. --> <style name="AppTheme" parent="AppBaseTheme"> <!-- All customizations that are NOT specific to a particular API-level can go here. --> </style> . . </resources>
See the discussion in Styles and Themes.

With the preceding as introduction, let us now implement an app that illustrates the use of composite themes, demonstrates how the display theme can be changed at runtime by logic in the Java code, and demonstrates how to store theme settings so that they persist across sessions.

 

Creating the Project

Because we are illustrating several different capabilities with this app, it requires a number of (mostly small) Java and XML files. Rather than pasting in each according to our usual procedure, we are going to just download and install the project directly, and then we shall demonstrate how it works and describe why it works. So begin by retrieving the the project ThemesDemo from GitHub. Instructions for installing it in Android Studio may be found in Packages for All Projects.

 

Trying it Out

Launch the app on a device or emulator running at least API 19. On the opening screen the overflow menu on the Task Bar (three vertical dots at upper right) can be opened and Settings selected to give the preference headers screen shown below.



Each of the headers on this screen defines different preference options or links. Clicking on the Greeting Name header gives an option to input your user name, as illustrated in the below-left figure, while clicking on the Styles header gives the option to choose an app theme (customized version of Holo Light and Holo Dark, as discussed further below) or to choose whether the displayed username will be given in all caps, as illustrated in the figure below right.



The last two headers on the preferences header screen illustrate how to link to potentially relevant material. (These particular links belong more properly on the Help page, but are included here to illustrate a technique.)


The icons displayed to the left of the headers on the preferences headers screen are from resources such as icon1.png included in the res/drawable directory. In this example they are just simple drawings but in a realistic application you would want to replace them with more appropriate icons.

By toggling the theme between the two options given in the preferences, we change the display theme of the entire app. For example, the following two images show the opening screen displayed with the Custom Dark and Custom Light choices, respectively.



The following two images show the Help item of the top ActionBar menu displayed with the dark and light themes, respectively.



The following two images show the dialog window launched by the first button on the opening screen (with label Show Error Dialog,) with the dark and light themes, respectively.



Finally, on the second screen (launched by a click on the second button of the opening screen) we illustrate a technique where a click on one of the two buttons displayed there launches another activity directly, while a long press on the same button instead opens a dialog window in which information about the activity to potentially be launched is given and the user has the choice of cancelling the dialog or pressing the Select this Task button to launch the activity. The following figure illustrates this functionality with the light themes preferences set.



 

How it Works

The opening screen displays two buttons, the second of which opens a second screen and the first of which demonstrates the launch of a dialog box displaying customized error messages. The first button and its actions are standard XML layout, event handling, and use of Intents to launch new Activities that have been discussed in many other projects. The second button launches a floating Dialog using techniques that have been discussed in Dialogs, Alerts, and Notifications. The buttons have both OnPress and OnLongPress handlers attached to them, and the corresponding actions are defined in the methods of MainActivity.java.

Pressing the overflow menu button of the phone on the top bar while the opening screen is displayed opens an options menu with the following choices



Selection of Settings produces a preferences headers menu (see the first image above) and choices in the preferences headers menu cause parameters to be set and stored in SharedPreferences. The key class responsible for this in the present app is defined by Prefs.java.

The essentially new ingredients here are the definition of multiple compound display themes for the app, the use of logic within the Java classes that decides which theme to implement at runtime, based on parameters stored in SharedPreferences, and the use of a fragments-based API to manage setting and storing the SharedPreferences. The basic procedure for the first two is:

  1. In the file res/values/styles.xml we use the techniques described above under Themes 101 to define new compound themes by adding to a base theme additional attributes, using XML tags. The comments within the file res/values/styles.xml describe how that is done in the present case.

  2. Use logic in the Java code for each displayed screen to choose which theme to implement by checking the values of parameters stored by the user in SharedPreferences. A typical implementation of this logic is exemplified by the method toggleTheme() in MainActivity.java.

The preferences screen is opened by clicking Settings in the action bar overflow menu. This fires an Intent to launch the Prefs class (see the onOptionsItemSelected(MenuItem item) handler in MainActivity.java). The class Prefs extends PreferenceActivity, which is the base class allowing an activity to show a hierarchy of preferences to the user.


Prior to Android 3.0 (API level 11), PreferenceActivity implemented the display of a single set of preferences. Implementation of a single level of preferences now uses PreferenceFragment, and PreferenceActivity in its new mode now displays one or more headers of preference, with each preference header associated with a PreferenceFragment to display preferences associated with that header. By subclassing PreferenceActivity and implementing the method onBuildHeaders(List<Header> target) in Prefs.java, we implicitly switch Android into the new headers + fragments mode.

In the method onBuildHeaders(List<Header> target) we call loadHeadersFromResource with an argument specifying res/xml/preference_headers.xml as the resource. The file preference_headers.xml has the content


<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" > <header android:fragment="<YourNamespace>.themesdemo.Prefs$Prefs1Fragment" android:icon="@drawable/icon1" android:summary="Choose a display style." android:title="Styles" /> <header android:fragment="<YourNamespace>.themesdemo.Prefs$Prefs2Fragment" android:icon="@drawable/icon2" android:summary="Choose your greeting name." android:title="Greeting Name" > </header> <header android:icon="@drawable/icon3" android:summary="Webpage for styles and themes." android:title="Styles and Themes Tutorial" > <intent android:action="android.intent.action.VIEW" android:data="http://developer.android.com/guide/topics/ui/themes.html" /> </header> <header android:icon="@drawable/icon4" android:summary="A description of constructing this project" android:title="This Project" > <intent android:action="android.intent.action.VIEW" android:data="http://eagle.phys.utk.edu/guidry/android/themesDemo.html" /> </header> </preference-headers>

which implements the headers displayed in the first figure shown above. These headers define two kinds of actions. In the first header, the


android:fragment="<YourNamespace>.themesdemo.Prefs$Prefs1Fragment

attribute associates the header with a fragment defined by the static inner class Prefs1Fragment (which is defined inside Prefs.java and extends PreferenceFragment).


The preceding expression employs a standard Java construction. To reference a static inner class from outside the (outer) enclosing class we specify the fully qualified path to the outer class, add "$", and then add the name of the inner class.

The event handling is taken care of automatically by Android so when we click on the first header this causes the preference fragment defined by Prefs$Prefs1Fragment to be executed. Prefs1Fragment first uses the PreferenceManager to set default values of the preferences to be displayed by reading the initialization file res/values/preferences_initialize.xml, which has the content


<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > <CheckBoxPreference android:defaultValue="false" android:key="caps_pref" android:summary="Check to capitalize name" android:title="Capitalize Name" /> <ListPreference android:defaultValue="1" android:dialogTitle="Theme" android:entries="@array/labels_list_preference" android:entryValues="@array/values_list_preference" android:key="list_preference" android:summary="Set theme preference" android:title="Theme Preference" /> </PreferenceScreen>

This will initialize two widgets:

  1. A CheckBoxPreference, which implements checkbox widget functionality and stores a boolean indicating the state of the checkbox in SharedPreferences. The corresponding preference has a key defined by the

    android:key="caps_pref"
    attribute that will allow us to access its current value from all classes in the project by referencing this key with code like
     
    SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); boolean isChecked = sharedPref.getBoolean("caps_pref", false);
    which uses PreferenceManager to get an instance of SharedPreferences, and then uses its getBoolean method with the key "caps_pref" to retrieve the current value of the stored preference.

  2. A ListPreference, which is a Preference that displays a list of entries in a Dialog window. The labels and values for the list are stored in the arrays specified by the

    android:entries="@array/labels_list_preference" android:entryValues="@array/values_list_preference"
    attributes, which reference string arrays defined in res/values/arrays.xml:

    <?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="labels_list_preference"> <item>Custom Light Theme</item> <item>Custom Dark Theme</item> </string-array> <string-array name="values_list_preference"> <item>1</item> <item>2</item> </string-array> </resources>
    This widget will store a String in SharedPreferences corresponding to the values_list_references choice "1" or "2" with the key list_preferences that will permit this preference value to be accessed from all classes in the package.

String arrays are simple resources referenced by the name attribute. Thus, it is often convenient to place multiple string arrays in the same file, as we have done above in res/values/arrays.xml.

The inner class Prefs1Fragment then uses the method addPreferencesFromResources(id) that Prefs1Fragment inherits from PreferenceFragment to inflate the XML resource res/xml/fragmented_preferences.xml


<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > <PreferenceCategory android:title="Application Preferences" > <CheckBoxPreference android:key="caps_pref" android:summary="Check to capitalize name" android:title="Capitalize Name" /> <ListPreference android:dialogTitle="Theme" android:entries="@array/labels_list_preference" android:entryValues="@array/values_list_preference" android:key="list_preference" android:summary="Set theme preference" android:title="Theme Preference" /> </PreferenceCategory> </PreferenceScreen>

and adds the corresponding preference hierarchy to the current preference hierarchy. The resulting display on a phone looks like the figure below left, and since the preferences fragment implements automatically the event handling, clicking on the Theme Preference option gives the dialog choice shown in the figure below right.



The shared preferences under the second preference header (Greeting Name) are handled in a quite analogous manner through the fragment associated with the inner class Prefs2Fragment in Prefs.

 

Advantages of Fragment Preferences

The use of classes extending PreferenceActivity and PreferenceFragment to handle preferences is particularly useful because

  1. It provides a natural way to organize preferences in a hierarchy of headers that launch specific preference screens. In the example here we implemented only a few preferences so the advantage is not so obvious. But in a more complex app with many possible preferences this organization will make your app easier to use.

  2. Widget display and event handling are included by the framework; you don't have to define your own widgets or event handlers for them. These event handlers take care automagically of updating the SharedPreference object that holds the preferences for the app.

  3. The corresponding SharedPreference object and its stored variables can be accessed from any classes in your project using constructions like

    SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); isChecked = sharedPref.getBoolean("caps_pref", false); String lister = sharedPref.getString("list_preference", "1"); String myName = sharedPref.getString("edittext_preference", "");
    to retrieve the current values of name-value pairs stored in SharedPreferences. For example, the method toggleTheme() in MainActivity.java uses such accesses to SharedPreferences to determine the theme and the user name that it should display when the opening screen is launched.

  4. The use of fragments to lay out the preferences gives the Android device options for the optimal display to implement. As we saw above, on a phone the preference headers are laid out on one screen, and then when one of them is selected the content of that preference fragment is displayed on a new screen. The figures below illustrate for a Moto-X phone with a 4.7 inch screen:




    The left figure shows the header screen, and when Styles is chosen the phone replaces the view with the screen shown in the right figure, giving the preference options under that header. This workflow makes sense for a phone, since the amount of information in the preceding two figures (particularly if you have a lot of preferences to specify) is most conveniently displayed in two successive pieces.

    On the other hand, when the same preference screen is implemented on a large tablet, much more screen real estate is now available and Android may choose to lay things out differently. For example, selection of Settings on an emulator running a generic 10-inch tablet formfactor gives the following screen




    which lays the header screen out in the left column and on the same screen displays the result of selecting one of the preference headers in the right column. (But if one selects one of the bottom two headers linked to webpages, the webpage will display fullscreen in the browser on both the phone and tablet, since these selections use Intents to launch a new activities that are not part of the preferences hierarchy.) The use of fragments in the shared preferences has allowed the same code to implement displays in fundamentally different ways on two devices having substantially different amounts of screen space. (For a more general introduction to the use of fragments in facilitating nimble screen layout, see the Fragments project.)

For these reasons, it is strongly recommended that you implement shared preferences using fragments and a hierarchy of preference screens by subclassing PreferenceFragment and PreferenceActivity, as illustrated here.


The complete project for the application described above is archived on GitHub at the link ThemesDemo. Instructions for installing it in Android Studio may be found in Packages for All Projects.

Last modified: July 25, 2016


Previous  | Next  | Home