How do I create one Preference with an EditTextPreference and a Togglebutton?

14,859

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];
            }
        };
    }
}

TogglePreference without any fancy styling applied

Share:
14,859
CodePrimate
Author by

CodePrimate

SOreadytohelp

Updated on June 17, 2022

Comments

  • CodePrimate
    CodePrimate about 2 years

    What I'm trying to implement is basically and exact replica of the image below (the preferences that I've squared). Pressing anything to the left of the preference should open up a dialog. Pressing the togglebutton will disable/enable whatever I'm setting in this preference.

    I've been trying for hours now and I've come up empty-handed. How do I implement this in a PreferenceActivity?

    Preference

    EDIT: It seems people are misunderstanding my question. It is very important that I figure out how to solve my problem using a PreferenceActivity. NOT an Activity. I don't care whether I need to do it in XML or programatically. Just please refrain from providing me with answers that I cannot use within a or something similar.

    EDIT 2 : Added a bounty - I really need an answer to this