Just a note upfront: this is going to be a bit of a long answer, but my intention is to provide you with a good-to-go answer that you could literally copy and paste to get started.
This is actually not too hard to accomplish. Your best starting point would be to look up the implementation of a SwichPreference
on ICS. You will see it is fairly simple, with most of the work being done by a TwoStatePreference
superclass, which on its turn is also only available ICS. Fortunately, you can almost literally copy-paste (see all the way down in this answer) that class and build your own TogglePreference
(let's call it that for the sake of clarity) on top of that, using the SwitchPreference
implementation as guide.
What you'll get by doing that, is something like below. I added some explanation to each method so that I can limit my writing here.
TogglePreference.java
package mh.so.pref;
import mh.so.R;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ToggleButton;
/**
* A {@link Preference} that provides a two-state toggleable option.
* <p>
* This preference will store a boolean into the SharedPreferences.
*/
public class TogglePreference extends TwoStatePreference {
private final Listener mListener = new Listener();
private ExternalListener mExternalListener;
/**
* Construct a new TogglePreference with the given style options.
*
* @param context The Context that will style this preference
* @param attrs Style attributes that differ from the default
* @param defStyle Theme attribute defining the default style options
*/
public TogglePreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Construct a new TogglePreference with the given style options.
*
* @param context The Context that will style this preference
* @param attrs Style attributes that differ from the default
*/
public TogglePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Construct a new TogglePreference with default style options.
*
* @param context The Context that will style this preference
*/
public TogglePreference(Context context) {
this(context, null);
}
/** Inflates a custom layout for this preference, taking advantage of views with ids that are already
* being used in the Preference base class.
*/
@Override protected View onCreateView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return inflater.inflate(R.layout.toggle_preference_layout, parent, false);
}
/** Since the Preference base class handles the icon and summary (or summaryOn and summaryOff in TwoStatePreference)
* we only need to handle the ToggleButton here. Simply get it from the previously created layout, set the data
* against it and hook up a listener to handle user interaction with the button.
*/
@Override protected void onBindView(View view) {
super.onBindView(view);
ToggleButton toggleButton = (ToggleButton) view.findViewById(R.id.toggle_togglebutton);
toggleButton.setChecked(isChecked());
toggleButton.setOnCheckedChangeListener(mListener);
}
/** This gets called when the preference (as a whole) is selected by the user. The TwoStatePreference
* implementation changes the actual state of this preference, which we don't want, since we're handling
* preference clicks with our 'external' listener. Hence, don't call super.onClick(), but the onPreferenceClick
* of our listener. */
@Override protected void onClick() {
if (mExternalListener != null) mExternalListener.onPreferenceClick();
}
/** Simple interface that defines an external listener that can be notified when the preference has been
* been clicked. This may be useful e.g. to navigate to a new activity from your PreferenceActivity, or
* display a dialog. */
public static interface ExternalListener {
void onPreferenceClick();
}
/** Sets an external listener for this preference*/
public void setExternalListener(ExternalListener listener) {
mExternalListener = listener;
}
/** Listener to update the boolean flag that gets stored into the Shared Preferences */
private class Listener implements CompoundButton.OnCheckedChangeListener {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (!callChangeListener(isChecked)) {
// Listener didn't like it, change it back.
// CompoundButton will make sure we don't recurse.
buttonView.setChecked(!isChecked);
return;
}
TogglePreference.this.setChecked(isChecked);
}
}
}
The layout file for this example is simply a LinearLayout
with three elements in it, with the most interesting one being the ToggleButton
. The ImageView
and TextView
take advantage of the work the Preference
base class already does by using the appropriate ids in the Android namespace. That way, we don't have to worry about those. Do note that I'm pretty sure that the icon option wasn't added until Honeycomb, so you might just want to add it as a custom attribute to the TogglePreference
and manually set it so that it's always there. Just flick me a comment if you need any more concrete pointers for this approach.
Anyways, obviously you can modify the layout to any extend, and apply styling to your liking. For example, to have the ToggleButton
mimic a Switch
, you could change the background to some other StateListDrawable
and/or change or completely get rid of the on/off text.
toggle_preference_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight" >
<ImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="false"
android:focusableInTouchMode="false" />
<TextView
android:id="@android:id/summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:focusable="false"
android:focusableInTouchMode="false"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ToggleButton
android:id="@+id/toggle_togglebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="false"
android:focusableInTouchMode="false" />
</LinearLayout>
You can then use TogglePreference
just like any other Preference
in your PreferenceActivity
. By hooking up a listener, you can do anything you like when the user selects the preference, whilst at the same time clicking the actual ToggleButton
will toggle the boolean value in the SharedPreferences
.
DemoPreferenceActivity.java
package mh.so.pref;
import mh.so.R;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.widget.Toast;
public class DemoPreferenceActivity extends PreferenceActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.prefs);
TogglePreference toggle = (TogglePreference) findPreference("toggle_preference");
toggle.setExternalListener(new TogglePreference.ExternalListener() {
@Override public void onPreferenceClick() {
Toast.makeText(DemoPreferenceActivity.this, "You clicked the preference without changing its value", Toast.LENGTH_LONG).show();
}
});
}
}
Prefs.xml is nothing more but a single definition of above TogglePreference
. You can supply all the usual attributes in Android's namespace. Optionally you could also declare some custom attributes to exploit the built-in functionality of TwoStatePreference
to deal with the summaryOn
and summaryOff
texts.
Prefs.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<PreferenceCategory android:title="Toggle preferences" >
<mh.so.pref.TogglePreference xmlns:app="http://schemas.android.com/apk/res/mh.so"
android:key="toggle_preference"
android:summary="Summary"
android:icon="@drawable/icon" />
</PreferenceCategory>
</PreferenceScreen>
Finally, the backported TwoStatePreference class from ICS. It's hardly any different from the original one, for which you can find the source overhere.
package mh.so.pref;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Common base class for preferences that have two selectable states, persist a
* boolean value in SharedPreferences, and may have dependent preferences that are
* enabled/disabled based on the current state.
*/
public abstract class TwoStatePreference extends Preference {
private CharSequence mSummaryOn;
private CharSequence mSummaryOff;
private boolean mChecked;
private boolean mDisableDependentsState;
public TwoStatePreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public TwoStatePreference(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TwoStatePreference(Context context) {
this(context, null);
}
@Override
protected void onClick() {
super.onClick();
boolean newValue = !isChecked();
if (!callChangeListener(newValue)) {
return;
}
setChecked(newValue);
}
/**
* Sets the checked state and saves it to the {@link SharedPreferences}.
*
* @param checked The checked state.
*/
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
persistBoolean(checked);
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
/**
* Returns the checked state.
*
* @return The checked state.
*/
public boolean isChecked() {
return mChecked;
}
@Override
public boolean shouldDisableDependents() {
boolean shouldDisable = mDisableDependentsState ? mChecked : !mChecked;
return shouldDisable || super.shouldDisableDependents();
}
/**
* Sets the summary to be shown when checked.
*
* @param summary The summary to be shown when checked.
*/
public void setSummaryOn(CharSequence summary) {
mSummaryOn = summary;
if (isChecked()) {
notifyChanged();
}
}
/**
* @see #setSummaryOn(CharSequence)
* @param summaryResId The summary as a resource.
*/
public void setSummaryOn(int summaryResId) {
setSummaryOn(getContext().getString(summaryResId));
}
/**
* Returns the summary to be shown when checked.
* @return The summary.
*/
public CharSequence getSummaryOn() {
return mSummaryOn;
}
/**
* Sets the summary to be shown when unchecked.
*
* @param summary The summary to be shown when unchecked.
*/
public void setSummaryOff(CharSequence summary) {
mSummaryOff = summary;
if (!isChecked()) {
notifyChanged();
}
}
/**
* @see #setSummaryOff(CharSequence)
* @param summaryResId The summary as a resource.
*/
public void setSummaryOff(int summaryResId) {
setSummaryOff(getContext().getString(summaryResId));
}
/**
* Returns the summary to be shown when unchecked.
* @return The summary.
*/
public CharSequence getSummaryOff() {
return mSummaryOff;
}
/**
* Returns whether dependents are disabled when this preference is on ({@code true})
* or when this preference is off ({@code false}).
*
* @return Whether dependents are disabled when this preference is on ({@code true})
* or when this preference is off ({@code false}).
*/
public boolean getDisableDependentsState() {
return mDisableDependentsState;
}
/**
* Sets whether dependents are disabled when this preference is on ({@code true})
* or when this preference is off ({@code false}).
*
* @param disableDependentsState The preference state that should disable dependents.
*/
public void setDisableDependentsState(boolean disableDependentsState) {
mDisableDependentsState = disableDependentsState;
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getBoolean(index, false);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setChecked(restoreValue ? getPersistedBoolean(mChecked)
: (Boolean) defaultValue);
}
/**
* Sync a summary view contained within view's subhierarchy with the correct summary text.
* @param view View where a summary should be located
*/
void syncSummaryView(View view) {
// Sync the summary view
TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
if (summaryView != null) {
boolean useDefaultSummary = true;
if (mChecked && mSummaryOn != null) {
summaryView.setText(mSummaryOn);
useDefaultSummary = false;
} else if (!mChecked && mSummaryOff != null) {
summaryView.setText(mSummaryOff);
useDefaultSummary = false;
}
if (useDefaultSummary) {
final CharSequence summary = getSummary();
if (summary != null) {
summaryView.setText(summary);
useDefaultSummary = false;
}
}
int newVisibility = View.GONE;
if (!useDefaultSummary) {
// Someone has written to it
newVisibility = View.VISIBLE;
}
if (newVisibility != summaryView.getVisibility()) {
summaryView.setVisibility(newVisibility);
}
}
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
if (isPersistent()) {
// No need to save instance state since it's persistent
return superState;
}
final SavedState myState = new SavedState(superState);
myState.checked = isChecked();
return myState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state == null || !state.getClass().equals(SavedState.class)) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state);
return;
}
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
setChecked(myState.checked);
}
static class SavedState extends BaseSavedState {
boolean checked;
public SavedState(Parcel source) {
super(source);
checked = source.readInt() == 1;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(checked ? 1 : 0);
}
public SavedState(Parcelable superState) {
super(superState);
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}