Custom view component in a DialogPreference
Posted 10-12-2009 at 11:08 PM by divestoclimb
There are several sites out there (like these) that show you how to use Android preferences, but I haven't seen anyone explain how to extend DialogPreference to get your own custom preference stored. I had to dig into the Android source to figure this out since reading Google's documentation on DialogPreference is like reading Greek!
I did this twice in Gas Mixer; once using my NumberSelector component I explained in my last posts, and also with my TrimixSelector. Because the TrimixSelector, in a way, represents a more likely scenario someone would run into when trying to build a custom preference, I'm going to go into step-by-step detail of how I did that. A TrimixSelector is what allows the user to pick the percentages of oxygen and helium that are in a gas. This is really two pieces of information, though, and it can't be encapsulated into a single value without resorting to a little hack... here's an example from my res/xml/preferences.xml:
I defined a TrimixPreference class which can then be inserted straight into preferences.xml. The only tricky part is the defaultValue, which I'll explain later. As an aside, you can make you own custom attributes here just like for any custom component. I did this with my NumberSelector's preference, NumberPreference.
In order to understand TrimixPreference, we have to look at the public methods of TrimixSelector:
TrimixSelector uses a custom Mix object as its interface for gets and sets of the mix. The data within Mix is really just two doubles, the percentages of oxygen and helium. There's also a change listener defined, but that's not important here.
Now we can talk about TrimixPreference, defined in Gas Mixer's TrimixPreference.java. Here's the basic class declaration, member variables, and constructors:
We extend DialogPreference, then create a TrimixSelector object within the class. In most cases, the constructor that gets called to instantiate our class is the second one, with no style specified; it's important that you use android.R.attr.dialogPreferenceStyle in that case.
The preference does internal storage of what the mix is as an intermediate step between the TrimixSelector View that the user manipulates, the Preference Manager's persisted preference value, and the saved state.
Because I had to choose a standard type to store my two mix percentages in, I decided to concatenate both together in a string. So I have methods that let me translate back and forth between strings like you see in the XML's defaultValue attribute and my Mix object, in addition to a getter and setter for the Mix object itself:
persistString() is somehow important to the Preference Manager, but it's not clearly documented... I copied this technique from Android sources and it works this way 
Now we come to the DialogPreference overrides. These are the methods you must override in order for this to work:
onCreateDialogView
This method normally would construct all of our views. Because it's possible for my custom object to have attributes passed to it from preferences.xml, I instantiated it up in the constructor. So all I have to do here is return the member variable as the view:
There's one other thing to keep in mind because of how I'm doing this that will come up later.
onGetDefaultValue and onSetInitialValue
These methods get and retrieve the preference value from the Preference Manager. I have to resort to strings again here, and store the result in my member variables.
onBindDialogView
This method takes the View and adds our data to it. Pretty simple here:
onDialogResult
This method is called when the dialog is about to close. It's called with a single argument that says whether the result is "positive", i.e. a generalization of whether the user pressed "OK" or "Cancel". Our override needs to save the value in the custom component if positiveResult is true:
callChangeListener() is yet another one of those poorly explained methods. I took this implementation out of the Android source.
removeView() is called because I'm not following the Android convention of creating my View in onCreateView. Without that last line, the dialog will work the first time but if the user opened it again a force close will result because mTrimixSelector is already bound to a view. This is cleanup, and as a plus I don't have to recreate my TrimixSelector when the dialog reopens.
onSaveInstanceState and onRestoreInstanceState
These are the typical instance state methods, along with a SavedState object that can store and give back the mix string.
I won't copy and paste it here; the implementation came out of Android sources. Essentially we have to encapsulate the superclass's instance state into our own custom object, which also contains our mix string. On restore, we extract our data and pass the superclass the state it had saved for itself.
Reading the preference value
I need to be able to retrieve the topup mix as a Mix object. To do this in my Activities, I merely call the TrimixPreference stringToMix static method:
Otherwise, access is the same as any other value.
That's all you should need in order to build your own DialogPreference subclasses. I don't fully understand it all, but I was able to get it working. Admittedly there's still some cleanup and optimization to do.
I did this twice in Gas Mixer; once using my NumberSelector component I explained in my last posts, and also with my TrimixSelector. Because the TrimixSelector, in a way, represents a more likely scenario someone would run into when trying to build a custom preference, I'm going to go into step-by-step detail of how I did that. A TrimixSelector is what allows the user to pick the percentages of oxygen and helium that are in a gas. This is really two pieces of information, though, and it can't be encapsulated into a single value without resorting to a little hack... here's an example from my res/xml/preferences.xml:
Code:
<divestoclimb.gasmixer.TrimixPreference
android:key="topup_gas"
android:title="@string/topup_gas"
android:defaultValue="0.21 0"
android:dialogIcon="@drawable/topup_pick" />
In order to understand TrimixPreference, we have to look at the public methods of TrimixSelector:
Code:
public class TrimixSelector extends RelativeLayout
implements SeekBar.OnSeekBarChangeListener, NumberSelector.ValueChangedListener {
public static interface MixChangeListener {
abstract void onChange(TrimixSelector ts, Mix m);
}
public TrimixSelector(Context context);
public TrimixSelector(Context context, AttributeSet attrs);
public Mix getMix();
public void setMix(Mix m);
public void setOnMixChangeListener(MixChangeListener l);
}
Now we can talk about TrimixPreference, defined in Gas Mixer's TrimixPreference.java. Here's the basic class declaration, member variables, and constructors:
Code:
public class TrimixPreference extends DialogPreference {
private TrimixSelector mTrimixSelector;
private Mix mMix;
private String mMixString;
public TrimixPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTrimixSelector = new TrimixSelector(context, attrs);
}
public TrimixPreference(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.dialogPreferenceStyle);
}
public TrimixPreference(Context context) {
this(context, null);
}
The preference does internal storage of what the mix is as an intermediate step between the TrimixSelector View that the user manipulates, the Preference Manager's persisted preference value, and the saved state.
Because I had to choose a standard type to store my two mix percentages in, I decided to concatenate both together in a string. So I have methods that let me translate back and forth between strings like you see in the XML's defaultValue attribute and my Mix object, in addition to a getter and setter for the Mix object itself:
Code:
private String mixToString(Mix mix) {
NumberFormat nf = new DecimalFormat(".###");
return nf.format(mix.getfO2())+" "+nf.format(mix.getfHe());
}
public static Mix stringToMix(String s) {
String ss[] = s.split("\\s");
NumberFormat nf = new DecimalFormat(".###");
try {
return new Mix(nf.parse(ss[0]).floatValue(), nf.parse(ss[1]).floatValue());
} catch(ParseException e) { return null; }
}
public void setMix(String mixString) {
mMix = stringToMix(mixString);
mMixString = mixString;
persistString(mixString);
}
public Mix getMix() {
return mMix;
}

Now we come to the DialogPreference overrides. These are the methods you must override in order for this to work:
onCreateDialogView
This method normally would construct all of our views. Because it's possible for my custom object to have attributes passed to it from preferences.xml, I instantiated it up in the constructor. So all I have to do here is return the member variable as the view:
Code:
@Override
protected View onCreateDialogView() {
return mTrimixSelector;
}
onGetDefaultValue and onSetInitialValue
Code:
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getString(index);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setMix(restoreValue? getPersistedString(mMixString): (String)defaultValue);
}
onBindDialogView
This method takes the View and adds our data to it. Pretty simple here:
Code:
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
mTrimixSelector.setMix(getMix());
}
This method is called when the dialog is about to close. It's called with a single argument that says whether the result is "positive", i.e. a generalization of whether the user pressed "OK" or "Cancel". Our override needs to save the value in the custom component if positiveResult is true:
Code:
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if(positiveResult) {
String mixString = mixToString(mTrimixSelector.getMix());
if(callChangeListener(mixString)) {
setMix(mixString);
}
}
((ViewGroup)mTrimixSelector.getParent()).removeView(mTrimixSelector);
}
removeView() is called because I'm not following the Android convention of creating my View in onCreateView. Without that last line, the dialog will work the first time but if the user opened it again a force close will result because mTrimixSelector is already bound to a view. This is cleanup, and as a plus I don't have to recreate my TrimixSelector when the dialog reopens.
onSaveInstanceState and onRestoreInstanceState
These are the typical instance state methods, along with a SavedState object that can store and give back the mix string.
I won't copy and paste it here; the implementation came out of Android sources. Essentially we have to encapsulate the superclass's instance state into our own custom object, which also contains our mix string. On restore, we extract our data and pass the superclass the state it had saved for itself.
Reading the preference value
I need to be able to retrieve the topup mix as a Mix object. To do this in my Activities, I merely call the TrimixPreference stringToMix static method:
Code:
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
mTopup = TrimixPreference.stringToMix(settings.getString("topup_gas", "0.21 0"));
That's all you should need in order to build your own DialogPreference subclasses. I don't fully understand it all, but I was able to get it working. Admittedly there's still some cleanup and optimization to do.
Total Comments 0







