forevercrashed
forevercrashed

Reputation: 135

Switch Preference - Handling both onPreferenceChange and onPreferenceClick

I've been trying to get a switch preference working in Android whereby I can intercept and handle differently, in certain cases, when they switch it on/off vs when they click the whole preference.

This is what I'm trying to accomplish: User goes into preferences tags are off and no tags are stored (ie: tag preference is empty) User turns on preference for tags, and since no tags are stored currently it launches a tag search activity for user to find the tag. - works fine.

If tag already exists, and they change the state ONLY then update the value as normal. - works fine

Here's my issue: If they click the preference though and they already have a tag saved, don't change the state (regardless if it's enabled or disabled), launch the tag search activity. - this DOESN'T work.

What I've found so far is that in the final scenario above, I get a call to onPreferenceChanged, followed by a call to onPreferenceClicked, followed by a subsequent call to onPreferenceChanged. This seems to be my problem. The first call to onPreferenceChanged causes my listener on my SharedPreferences to be called telling it that it's now enabled.

If I didn't receive the first call to onPreferenceChanged then I wouldn't have an issue.

Here is the relevant parts where I'm setting the listeners

SwitchPreference tagPref = (SwitchPreference) findPreference(PreferencesConstants.PREFERENCE_TAG_ENABLED);
    tagPref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

        @Override
        public boolean onPreferenceChange(Preference preference, Object newValue) {
            Log.e("BLAH", "onPrefChanged....is it handled by OnClick?" + Boolean.toString(handledByClick));


            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());

            boolean enabled = prefs.getBoolean(PreferencesConstants.PREFERENCE_TAG_ENABLED, false);
            Log.e("BLAH", "value stored in prefs? " + Boolean.toString(enabled));
            if (newValue instanceof Boolean) {
                enabled = (Boolean) newValue;
            }

            Log.e("BLAH", "New value? " + Boolean.toString(enabled));

            if (!handledByClick) {
                if (enabled && (currentTag == null || currentTag.isEmpty())) {
                    Log.e("BLAH", "Enabled and CurrentTag empty!");
                    Intent intent = new Intent(getActivity(), TagSearchActivity.class);
                    startActivityForResult(intent, 0);

                    return false; // always return false, we'll handle
                                    // updating
                                    // this value manually.
                } else {
                    return true;
                }
            }
            Log.e("BLAH", "returning false (AS IN WE HANDLED IT).");
            return false;
        }
    });

    tagPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {

        @Override
        public boolean onPreferenceClick(Preference preference) {

            handledByClick = true;
            Log.e("BLAH", "onprefClick");

            Intent intent = new Intent(getActivity(), TagSearchActivity.class);
            startActivityForResult(intent, 0);

            return true;
        }
    });

Here are the relevant log lines after running it with a saved tag, and clicking the preference.

01-18 15:55:05.593: E/BLAH(13261): onPrefChanged....is it handled by OnClick?false
01-18 15:55:05.593: E/BLAH(13261): value stored in prefs? true
01-18 15:55:05.593: E/BLAH(13261): New value? false
01-18 15:55:05.613: E/DifferentClass(13261): On Shared Preferences Changed - tagEnabled
01-18 15:55:05.652: E/DifferentClass(13261): disabled TAG in cancelAlarmService
01-18 15:55:05.662: E/AnotherClass(13261): Updating Feed List.  Old Size: 33, New Size: 14
01-18 15:55:05.682: E/BLAH(13261): onprefClick
01-18 15:55:05.812: E/BLAH(13261): onPrefChanged....is it handled by OnClick?true
01-18 15:55:05.812: E/BLAH(13261): value stored in prefs? false
01-18 15:55:05.822: E/BLAH(13261): New value? false
01-18 15:55:05.822: E/BLAH(13261): returning false (AS IN WE HANDLED IT).

Upvotes: 8

Views: 15141

Answers (4)

Mendhak
Mendhak

Reputation: 8775

This took me ages, and none of the answers here worked. I finally found the simplest answer, no custom layout required, in a Github Gist, which I'll preserve here, but I have modified it to work with SwitchPreferenceCompat instead of SwitchPreference.

First, create the custom preference.

package com.mendhak.gpslogger.ui.components;


import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;


import androidx.appcompat.widget.SwitchCompat;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreferenceCompat;

/**
 * Custom preference for handling a switch with a clickable preference area as well
 */
public class SwitchPlusClickPreference extends SwitchPreferenceCompat {

    //
    // Public interface
    //

    /**
     * Sets listeners for the switch and the background container preference view cell
     * @param listener A valid SwitchPlusClickListener
     */
    public void setSwitchClickListener(SwitchPlusClickListener listener){
        this.listener = listener;
    }
    private SwitchPlusClickListener listener = null;

    /**
     * Interface gives callbacks in to both parts of the preference
     */
    public interface SwitchPlusClickListener {
        /**
         * Called when the switch is switched
         * @param buttonView
         * @param isChecked
         */
        public void onCheckedChanged(SwitchCompat buttonView, boolean isChecked);

        /**
         * Called when the preference view is clicked
         * @param view
         */
        public void onClick(View view);
    }

    public SwitchPlusClickPreference(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public SwitchPlusClickPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SwitchPlusClickPreference(Context context) {
        super(context);
    }


    //
    // Internal Functions
    //

    /**
     * Recursively go through view tree until we find an android.widget.Switch
     * @param view Root view to start searching
     * @return A Switch class or null
     */
    private SwitchCompat findSwitchWidget(View view){
        if (view instanceof  SwitchCompat){
            return (SwitchCompat)view;
        }
        if (view instanceof ViewGroup){
            ViewGroup viewGroup = (ViewGroup)view;
            for (int i = 0; i < viewGroup.getChildCount();i++){
                View child = viewGroup.getChildAt(i);
                if (child instanceof ViewGroup){
                    SwitchCompat result = findSwitchWidget(child);
                    if (result!=null) return result;
                }
                if (child instanceof SwitchCompat){
                    return (SwitchCompat)child;
                }
            }
        }
        return null;
    }

    @Override
    public void onBindViewHolder(PreferenceViewHolder holder) {
        super.onBindViewHolder(holder);
        final SwitchCompat switchView = findSwitchWidget(holder.itemView);
        if (switchView!=null){
            switchView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (listener != null)
                        listener.onCheckedChanged((SwitchCompat) v, ((SwitchCompat)v).isChecked());
                }
            });
            switchView.setChecked(getSharedPreferences().getBoolean(getKey(),false));
            switchView.setFocusable(true);
            switchView.setEnabled(true);
            //Set the thumb drawable here if you need to. Seems like this code makes it not respect thumb_drawable in the xml.
        }

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener!=null) listener.onClick(v);
            }
        });
    }

//    //Get a handle on the 2 parts of the switch preference and assign handlers to them
//    @Override
//    protected void onBindView (View view){
//        super.onBindView(view);
//
//        final Switch switchView = findSwitchWidget(view);
//        if (switchView!=null){
//            switchView.setOnClickListener(new View.OnClickListener() {
//                @Override
//                public void onClick(View v) {
//                    if (listener != null)
//                        listener.onCheckedChanged((Switch) v, ((Switch)v).isChecked());
//                }
//            });
//            switchView.setChecked(getSharedPreferences().getBoolean(getKey(),false));
//            switchView.setFocusable(true);
//            switchView.setEnabled(true);
//            //Set the thumb drawable here if you need to. Seems like this code makes it not respect thumb_drawable in the xml.
//        }
//
//        view.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//                if (listener!=null) listener.onClick(v);
//            }
//        });
//    }
}

Next in your preference XML, add an entry:

<com.mendhak.gpslogger.ui.components.SwitchPlusClickPreference
            android:key="google_drive_enabled"
            android:title="@string/google_drive_setup_title"
            android:icon="@drawable/googledrive"/>

And then in the code for your Preference Activity class, the real magic is that you use the built-in callbacks for changed, and clicked.

((SwitchPlusClickPreference)findPreference(PreferenceNames.AUTOSEND_GOOGLE_DRIVE_ENABLED)).setSwitchClickListener(new SwitchPlusClickPreference.SwitchPlusClickListener() {

            @Override
            public void onCheckedChanged(SwitchCompat buttonView, boolean isChecked) {
                //The switch bit changed. handle it. 
            }

            @Override
            public void onClick(View view) {
                // The text bit was clicked on, handle it
            }
        });


Upvotes: 0

Yokich
Yokich

Reputation: 1326

I have been working with the same issue for ages now and you can go about it two ways.

Implementing a switchpreference with custom actions for every event:

  • forevercrashed made some good points. I tried follow them, but for me they didn't do it. I bet they work, but I needed more functionallity in an easier way. Xgouchet (second Link) uses Headers and custom xml layouts which uses custom placements and measurements (height, witdth, padding etc.). I needed a solution without altering Googles built in auto-generated layout.

  • The super easy and powerful way: implement your own SwitchPreference! Just make a class extend SwitchPreference and then implement/override like so:

    public class AutoUploadSwitchPreference extends SwitchPreference {
    public AutoUploadSwitchPreference(Context context) {
        super(context);
    }
    public AutoUploadSwitchPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public AutoUploadSwitchPreference(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    
    @Override
    protected void onClick() {
        //super.onClick(); THIS IS THE IMPORTANT PART!
    }
    

By overriding onClick() and commenting out / deleting super.onClick() makes the SwitchPreference NOT call callChangeListener(Object newValue). Now you can click the preference and nothing happens, not until you want it to. (One bug that would occur otherwise was having multiple calls to onPreferenceChange in the fragment)


Now! To make things happen: Here is the structure I have used.

  • Create a SettingsActivity
    • In it make sure you fetch preferences, resources etc.
  • in onCreate() in your Activity - launch a PreferenceFragment
    • This needs to be a custom class extending PreferenceFragment, see how here : PreferenceFragment
  • In your custom Fragment, get hold of your custom-preference. You can use findPreference("custom_switch_key").

    • add an OnPreferenceChangeListener on the preference
    • I personally make my fragment implement the listener and pass this as argument.
    • The return statement is important. This is what makes the actual change in the switch. If you return true the switch will change into the newValue. If you return false, it will not. If you use return false; you can change the value with setChecked(true|false) on the switchpreference.
  • when you implement onPreferenceChange(Preference preference, Object newValue) you can add whatever functionality you want from pressing the switch-slider only

  • the functionality from clicking the preference can be done in three ways:
    • Implement the onClick() further in the custom SwitchPreference class
    • Implement the method onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) in the fragment
    • Implement an onPreferenceClickListener like you did for the ChangeListener.

Sorry if this is a long post. It is my first and I have been through so many stackoverflow-pages about this and no one was accurate, just wanted to get it right ;)

Upvotes: 8

forevercrashed
forevercrashed

Reputation: 135

After searching for hours more I came across a couple posts that will be helpful to others in this situation.

This one was the solution I opted for given my problem: How do I create one Preference with an EditTextPreference and a Togglebutton?

It's a very detailed answer and is very helpful in understanding preferences.

The other post I came across was this one: http://xgouchet.fr/android/index.php?article4/master-on-off-preferences-with-ice-cream-sandwich

It will give you pretty much the same look and feel as the one above, but requires more work and because of my requirements wouldn't work for me.

Upvotes: 2

android developer
android developer

Reputation: 116332

i think you are asking about a feature that doesn't exist.

however , since the preference activity uses a listView , you can use some tricks to customize it and handle it however you wish .

here's a post i've made about customizing it , based on this website . what i've asked there is how to add a listView , but i didn't know that a preference activity actually uses a listview .

Upvotes: 1

Related Questions