kyleduo / kyleduo/SwitchButton

SwitchButton used in a custom SwitchPreference

Open
#69 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Java
Stars
4.7k
Forks
864
PR merge metrics
No merged PRs in 30d

Description

Hi there,

I am using the SwitchButton in a subclass of SwitchPreference, and then this preference instance is added with other standard ones (EditTextPreference, Preference,...) in a PreferenceFragment.
My SwitchButton is implemented using a widget layout like this :

<?xml version="1.0" encoding="utf-8"?>
<com.kyleduo.switchbutton.SwitchButton
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    style="@style/SwitchButton"
    android:id="@+id/switch_button"
    android:layout_width="50dp"
    android:layout_height="30dp"
    android:layout_gravity="center_vertical|end">
</com.kyleduo.switchbutton.SwitchButton>

The customization of preferences can be a little bit tricky, but I think my implementation is correct. Here is what this CustomSwitchPreference looks like :

public class CustomSwitchPreference extends SwitchPreference implements SwitchButton.OnCheckedChangeListener {

    private SwitchButton mSwitchButton;
    private boolean mChecked;

    public CustomSwitchPreference(Context context) {
        this(context, null);
    }

    public CustomSwitchPreference(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        // Inflate the custom preference layout and its custom widget.
        setLayoutResource(R.layout.pref_layout);
        setWidgetLayoutResource(R.layout.pref_switch_widget);
    }

    @Override
    protected Object onGetDefaultValue(TypedArray a, int index) {
        boolean defaultValue = a.getBoolean(index, false);
        return defaultValue;
    }

    @Override
    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
        if (restorePersistedValue) {
            // Restore existing state
            boolean persistingCheck = getPersistedBoolean(mChecked);
            setChecked(persistingCheck);
        } else {
            // Set default state from the XML attribute
            setChecked((Boolean)defaultValue);
        }
    }

    @Override
    protected void onBindView(View view) {
        super.onBindView(view);

        mSwitchButton = (SwitchButton) view.findViewById(R.id.switch_button);
        mSwitchButton.setOnCheckedChangeListener(this);
    }

    public boolean isChecked() {
        return mChecked;
    }

    public void setChecked(boolean checked) {
        if (checked != mChecked) {
            mChecked = checked;
            persistBoolean(mChecked);

            notifyChanged();
            notifyDependencyChange(mChecked);
        }
    }

    /*
     * SwitchButton.OnCheckedChangeListener implementation.
     */
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (callChangeListener(isChecked)) {
            setChecked(isChecked);
        }
    }

    /*
     * Preference's lifecycle to save and restore the state.
     */
    @Override
    protected Parcelable onSaveInstanceState() {
        final Parcelable superState = super.onSaveInstanceState();
        // Check whether this Preference is persistent (continually saved).
        if (isPersistent()) {
            // No need to save instance state since it's persistent, use superclass state.
            return superState;
        }
        // Create instance of custom BaseSavedState
        final SavedState myState = new SavedState(superState);
        // Set the state's value with the class member that holds current setting value.
        myState.checked = mChecked;
        return myState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        // Check whether we saved the state in onSaveInstanceState.
        if (state == null || !state.getClass().equals(SavedState.class)) {
            // Didn't save the state, so call superclass.
            super.onRestoreInstanceState(state);
            return;
        }
        // Cast state to custom BaseSavedState and pass to superclass
        SavedState myState = (SavedState) state;
        super.onRestoreInstanceState(myState.getSuperState());

        // Set this Preference's widget to reflect the restored state
        setChecked(myState.checked);
    }

    /*
     * Saving and restoring the Preference's state.
     */
    private static class SavedState extends BaseSavedState {
        // Member that holds the setting's value.
        boolean checked;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        public SavedState(Parcel source) {
            super(source);
            // Get the current preference's value.
            checked = (Boolean) source.readValue(getClass().getClassLoader());
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            // Write the preference's value.
            dest.writeValue(checked);
        }

        // Standard creator object using an instance of this class.
        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];
            }
        };
    }
}

When I test all this in my app, everything is fine only if notifyChanged() and/or notifyDependencyChanged() are not called in method setChecked(boolean checked). If one of these notification methods is called, the SwitchButton starts to misbehave :

  • if state is changed from true to false, the state changes immediately even if the SwitchButton is set to have an animation during transition.
  • then if the state is changed from false to true, nothing happens... but in fact, internally, the state has changed.
  • finally, I have to click one more time on the SwitchButton to see the animation occur (the internal state remain to true).

I have tried many workaround, but cannot find one to fix this.
As the behavior is not the same depending on the state of the SwitchButton, I suspect that internally something is managed differently when the UI of the Preferences objects is updated.
I hope you could have a look at this and try to reproduce the case. Using the SwitchButton in Preferences is something quite normal and natural, but it is then important that the Switch behaves as expected when notifyChanged() or notifyDependencyChanged() are called.

Thanks in advance!

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the report with CustomSwitchPreference in a PreferenceFragment using pref_layout and pref_switch_widget, then trace setChecked(boolean) through notifyChanged() and notifyDependencyChange(). Verify that true-to-false and false-to-true changes both retain the SwitchButton animation and synchronized internal state after preference updates.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, java
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.