Custom Dialogs and Preferences

Android ships with a number of dialog classes for specific circumstances, like DatePickerDialog and ProgressDialog. Similarly, Android comes with a smattering of Preference classes for your PreferenceActivity, to accept text or selections from lists and so on.

However, there is plenty of room for improvement in both areas. As such, you may find the need to create your own custom dialog or preference class. This chapter will show you how that is done.

We start off by looking at creating a custom AlertDialog, not by using AlertDialog.Builder, but via a custom subclass. Then, we show how to create your own dialog-style Preference, where tapping on the preference pops up a dialog to allow the user to customize the preference value.

Prerequisites

Understanding this chapter requires that you have read the chapter on dialogs, along with the chapter on the preference system. Also, the samples here use the custom ColorMixer View described in another chapter.

Your Dialog, Chocolate-Covered

For your own application, the simplest way to create a custom AlertDialog is to use AlertDialog.Builder, as described in a previous chapter. You do not need to create any special subclass — just call methods on the Builder, then show() the resulting dialog.

However, if you want to create a reusable AlertDialog, this may become problematic. For example, where would this code to create the custom AlertDialog reside?

So, in some cases, you may wish to extend AlertDialog and supply the dialog’s contents that way, which is how TimePickerDialog and others are implemented. Unfortunately, this technique is not well documented. This section will illustrate how to create such an AlertDialog subclass, as determined by looking at how the core Android team did it for their own dialogs.

The sample code is ColorMixerDialog, a dialog wrapping around the ColorMixer widget shown in a previous chapter. The implementation of ColorMixerDialog can be found in the CWAC-ColorMixer GitHub repository, as it is part of the CommonsWare Android Components.

Using this dialog works much like using DatePickerDialog or TimePickerDialog. You create an instance of ColorMixerDialog, supplying the initial color to show and a listener object to be notified of color changes. Then, call show() on the dialog. If the user makes a change and accepts the dialog, your listener will be informed.

The ColorMixerDialog
Figure 645: The ColorMixerDialog

Basic AlertDialog Setup

The ColorMixerDialog class is not especially long, since all of the actual color mixing is handled by the ColorMixer widget:

package com.commonsware.cwac.colormixer;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;

public class ColorMixerDialog extends AlertDialog
  implements DialogInterface.OnClickListener {
  static private final String COLOR="c";
  private ColorMixer mixer=null;
  private int initialColor;
  private ColorMixer.OnColorChangedListener onSet=null;
  
  public ColorMixerDialog(Context ctxt,
                          int initialColor,
                          ColorMixer.OnColorChangedListener onSet) {
    super(ctxt);
    
    this.initialColor=initialColor;
    this.onSet=onSet;
    
    mixer=new ColorMixer(ctxt);
    mixer.setColor(initialColor);
    
    setView(mixer);
    setButton(ctxt.getText(R.string.cwac_colormixer_set),
              this);
    setButton2(ctxt.getText(R.string.cwac_colormixer_cancel),
               (DialogInterface.OnClickListener)null);
  }
  
  @Override
  public void onClick(DialogInterface dialog, int which) {
    if (initialColor!=mixer.getColor()) {
      onSet.onColorChange(mixer.getColor());
    }
  }
  
  @Override
  public Bundle onSaveInstanceState() {
    Bundle state=super.onSaveInstanceState();
    
    state.putInt(COLOR, mixer.getColor());
    
    return(state);
  }

  @Override
  public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
      
    mixer.setColor(state.getInt(COLOR));
  }
}

We extend the AlertDialog class and implement a constructor of our own design. In this case, we take in three parameters:

  1. A Context (typically an Activity), needed for the superclass
  2. The initial color to use for the dialog, such as if the user is editing a color they chose before
  3. A ColorMixer.OnColorChangedListener object, just like ColorMixer uses, to notify the dialog creator when the color is changed

We then create a ColorMixer and call setView() to make that be the main content of the dialog. We also call setButton() and setButton2() to specify a “Set” and “Cancel” button for the dialog. The latter just dismisses the dialog, so we need no event handler. The former we route back to the ColorMixerDialog itself, which implements the DialogInterface.OnClickListener interface.

Handling Color Changes

When the user clicks the “Set” button, we want to notify the application about the color change…if the color actually changed. This is akin to DatePickerDialog and TimePickerDialog only notifying you of date or times if the user clicks Set and actually changed the values.

The ColorMixerDialog tracks the initial color via the initialColor data member. In the onClick() method — required by DialogInterface.OnClickListener — we see if the mixer has a different color than the initialColor, and if so, we call the supplied ColorMixer.OnColorChangedListener callback object:

  @Override
  public void onClick(DialogInterface dialog, int which) {
    if (initialColor!=mixer.getColor()) {
      onSet.onColorChange(mixer.getColor());
    }
  }

State Management

Dialogs use onSaveInstanceState() and onRestoreInstanceState(), just like activities do. That way, if the screen is rotated, or if the hosting activity is being evicted from RAM when it is not in the foreground, the dialog can save its state, then get it back later as needed.

The biggest difference with onSaveInstanceState() for a dialog is that the Bundle of state data is not passed into the method. Rather, you get the Bundle by chaining to the superclass, then adding your data to the Bundle it returned, before returning it yourself:

  @Override
  public Bundle onSaveInstanceState() {
    Bundle state=super.onSaveInstanceState();
    
    state.putInt(COLOR, mixer.getColor());
    
    return(state);
  }

The onRestoreInstanceState() pattern is much closer to the implementation you would find in an Activity, where the Bundle with the state data to restore is passed in as a parameter:

  @Override
  public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
      
    mixer.setColor(state.getInt(COLOR));
  }

Preferring Your Own Preferences, Preferably

The Android Settings application, built using the Preference system, has lots of custom Preference classes. You too can create your own Preference classes, to collect things like dates, numbers, or colors. Once again, though, the process of creating such classes is not well documented. This section reviews one recipe for making a Preference — specifically, a subclass of DialogPreference – based on the implementation of other Preference classes in Android.

The result is ColorPreference, a Preference that uses the ColorMixer widget. As with the ColorMixerDialog from the previous section, the ColorPreference is from the CommonsWare Android Components, and its source code can be found in the CWAC-ColorMixer GitHub repository.

One might think that ColorPreference, as a subclass of DialogPreference, might use ColorMixerDialog. However, that is not the way it works, as you will see.

The Constructor

A Preference is much like a custom View, in that there are a variety of constructors, some taking an AttributeSet (for the preference properties), and some taking a default style. In the case of ColorPreference, we need to get the string resources to use for the names of the buttons in the dialog box, providing them to DialogPreference via setPositiveButtonText() and setNegativeButtonText().

Here, we just implement the standard two-parameter constructor, since that is the one that is used when this preference is inflated from a preference XML file:

  public ColorPreference(Context ctxt, AttributeSet attrs) {
    super(ctxt, attrs);
    
    setPositiveButtonText(ctxt.getText(R.string.cwac_colormixer_set));
    setNegativeButtonText(ctxt.getText(R.string.cwac_colormixer_cancel));
  }

Creating the View

The DialogPreference class handles the pop-up dialog that appears when the preference is clicked upon by the user. Subclasses get to provide the View that goes inside the dialog. This is handled a bit reminiscent of a CursorAdapter, in that there are two separate methods to be overridden:

In the case of ColorPreference, we use a ColorMixer for the View:

  @Override
  protected View onCreateDialogView() {
    mixer=new ColorMixer(getContext());
    
    return(mixer);
  }

Then, in onBindDialogView(), we set the mixer’s color to be lastColor, a private data member:

  @Override
  protected void onBindDialogView(View v) {
    super.onBindDialogView(v);
    
    mixer.setColor(lastColor);
  }

We will see later in this section where lastColor comes from – for the moment, take it on faith that it holds the user’s chosen color, or a default value.

Dealing with Preference Values

Of course, the whole point behind a Preference is to allow the user to set some value that the application will then use later on. Dealing with values is a bit tricky with DialogPreference, but not too bad.

Getting the Default Value

The preference XML format has an android:defaultValue attribute, which holds the default value to be used by the preference. Of course, the actual data type of the value will differ widely — an EditTextPreference might expect a String, while ColorPreference needs a color value.

Hence, you need to implement onGetDefaultValue(). This is passed a TypedArray — similar to how a custom View uses a TypedArray for getting at its custom attributes in an XML layout file. It is also passed an index number into the array representing android:defaultValue. The custom Preference needs to return an Object representing its interpretation of the default value.

In the case of ColorPreference, we simply get an integer out of the TypedArray, representing the color value, with an overall default value of 0xFFA4C639 (a.k.a., Android green):

  @Override
  protected Object onGetDefaultValue(TypedArray a, int index) {
    return(a.getInt(index, 0xFFA4C639));
  }

Setting the Initial Value

When the user clicks on the preference, the DialogPreference supplies the last-known preference value to its subclass, or the default value if this preference has not been set by the user to date.

The way this works is that the custom Preference needs to override onSetInitialValue(). This is passed in a boolean flag (restoreValue) indicating whether or not the user set the value of the preference before. It is also passed the Object returned by onGetDefaultValue(). Typically, a custom Preference will look at the flag and choose to either use the default value or load the already-set preference value.

To get the existing value, Preference defines a set of type-specific getter methods — getPersistedInt(), getPersistedString(), etc. So, ColorPreference uses getPersistedInt() to get the saved color value:

  @Override
  protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
    lastColor=(restoreValue ? getPersistedInt(lastColor) : (Integer)defaultValue);
  }

Here, onSetInitialValue() stores that value in lastColor — which then winds up being used by onBindDialogView() to tell the ColorMixer what color to show.

Closing the Dialog

When the user closes the dialog, it is time to persist the chosen color from the ColorMixer. This is handled by the onDialogClosed() callback method on your custom Preference:

  @Override
  protected void onDialogClosed(boolean positiveResult) {
    super.onDialogClosed(positiveResult);

    if (positiveResult) {
      if (callChangeListener(mixer.getColor())) {
        lastColor=mixer.getColor();
        persistInt(lastColor);
      }
    }
  }

The passed-in boolean indicates if the user accepted or dismissed the dialog, so you can elect to skip saving anything if the user dismissed the dialog. The other DialogPreference implementations also call callChangeListener(), which is somewhat ill-documented. Assuming both the flag and callChangeListener() are true, the Preference should save its value to the persistent store via persistInt(), persistString(), or kin.

Using the Preference

Given all of that, using the custom Preference class in an application is almost anti-climactic. You simply add it to your preference XML, with a fully-qualified class name:

<PreferenceScreen
  xmlns:android="http://schemas.android.com/apk/res/android">
  <com.commonsware.cwac.colormixer.ColorPreference
    android:key="favoriteColor"
    android:defaultValue="0xFFA4C639"
    android:title="Your Favorite Color"
    android:summary="Blue.  No yel--  Auuuuuuuugh!" />
</PreferenceScreen>

At this point, it behaves no differently than does any other Preference type. Since ColorPreference stores the value as an integer, your code would use getInt() on the SharedPreferences to retrieve the value when needed.

The user sees an ordinary preference entry in the PreferenceActivity:

A PreferenceActivity, showing the ColorPreference
Figure 646: A PreferenceActivity, showing the ColorPreference

When tapped, it brings up the mixer:

The ColorMixer in a custom DialogPreference
Figure 647: The ColorMixer in a custom DialogPreference

Choosing a color and clicking “Set” persists the color value as a preference.