How can I distinguish whether Switch,Checkbox Value is changed by user or programmatically (including by retention)?
Asked Answered
P

16

127
setOnCheckedChangeListener(new OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                // How to check whether the checkbox/switch has been checked
                // by user or it has been checked programatically ?

                if (isNotSetByUser())
                    return;
                handleSetbyUser();
            }
        });

How to implement method isNotSetByUser()?

Perseus answered 3/2, 2012 at 14:5 Comment(3)
I am not certain, but I think if the user toggled it then you'd get an onClick callback too if you set that listener. So maybe you can set but a boolean flag in the onClick that way you can check it in onCheckChanged to see if the user initiated the change.Terni
related Change Checkbox value without triggering onCheckChanged, proposed by krishanGinter
I have more simple and clear solution: see https://mcmap.net/q/138149/-how-to-know-whether-user-has-changed-the-state-of-toggle-buttonBabb
A
179

Answer 2:

A very simple answer:

Use on OnClickListener instead of OnCheckedChangeListener

    someCheckBox.setOnClickListener(new OnClickListener(){

        @Override
        public void onClick(View v) {
            // you might keep a reference to the CheckBox to avoid this class cast
            boolean checked = ((CheckBox)v).isChecked();
            setSomeBoolean(checked);
        }

    });

Now you only pick up click events and don't have to worry about programmatic changes.


Answer 1:

I have created a wrapper class (see Decorator Pattern) which handles this problem in an encapsulated way:

public class BetterCheckBox extends CheckBox {
    private CompoundButton.OnCheckedChangeListener myListener = null;
    private CheckBox myCheckBox;

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

    public BetterCheckBox(Context context, CheckBox checkBox) {
        this(context);
        this.myCheckBox = checkBox;
    }

    // assorted constructors here...    

    @Override
    public void setOnCheckedChangeListener(
        CompoundButton.OnCheckedChangeListener listener){
        if(listener != null) {
            this.myListener = listener;
        }
        myCheckBox.setOnCheckedChangeListener(listener);
    }

    public void silentlySetChecked(boolean checked){
        toggleListener(false);
        myCheckBox.setChecked(checked);
        toggleListener(true);
    }

    private void toggleListener(boolean on){
        if(on) {
            this.setOnCheckedChangeListener(myListener);
        }
        else {
            this.setOnCheckedChangeListener(null);
        }
    }
}

CheckBox can still be declared the same in XML, but use this when initializing your GUI in code:

BetterCheckBox myCheckBox;

// later...
myCheckBox = new BetterCheckBox(context,
    (CheckBox) view.findViewById(R.id.my_check_box));

If you want to set checked from code without triggering the listener, call myCheckBox.silentlySetChecked(someBoolean) instead of setChecked.

Arratoon answered 13/1, 2013 at 20:11 Comment(7)
For a Switch, answer 1 works in both tap and slide cases, whereas answer 2 will only work in the tap case. As a personal preference, I made my class extend CheckBox/Switch rather than holding a reference to one. Feels cleaner that way (note that you must specify the full package name in the XML if you do that).Nyberg
Thanks for this anthropomo, I don't know why I didn't think about it earlier, but you saved me some precious time ;). Cheers !Horsy
I am not sure about this, but if you extend SwitchCompat (using appcompat v7 ) to get the material design switch, you may end the new redesign and tinting capabilities.Doroteya
Both solutions have serious flaws. First solution: When the user drags the switch the listener does not get fired. Second Solution: Because setOnCheckedChangeListener does that null check it actuallly sets the old listener when there is a new already set.Wear
First solution: known and semi-acceptable issue if you want a concise solution and do not intent to use that widget. Second solution: changed the null check to address that kind of unlikely edge case. Now we assign listener to myListener whenever listener is not null. In almost all cases this does nothing, but if we make a new listener for some reason, that's not broken.Arratoon
An edit suggested referencing the new checkbox directly from XML rather than wrapping the standard checkbox. While this would work, it loses the Decorator advantage of decoupling the specific need of silent setting in the code from the desire to have clean, portable XML layouts.Arratoon
You can use this class with both Switch and CheckBox if you change myCheckBox type to CompoundButton (both Switch and CheckBox extend it).Poddy
C
37

Inside the onCheckedChanged() just check whether the user has actually checked/unchecked the radio button and then do the stuff accordingly as follows:

mMySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
 @Override
 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
   if (buttonView.isPressed()) {
       // User has clicked check box
    }
   else
    {
       //triggered due to programmatic assignment using 'setChecked()' method.   
    }
  }
});
Czarevna answered 6/10, 2017 at 10:16 Comment(5)
Good solution, no need to customize the view.Bugbear
this is not working when user is toggling the switch by swiping/slidingFichu
Using this approach everywhere but found a case it doesn't work as isPressed returns false, Nokia device with TalkBack on.Gaussmeter
SwitchMaterial work without problem. Good decision, thanks!Arrowworm
Agree with @Gaussmeter this returns falls when accessibility talkback is on. Tested on Samsung deviceWie
E
35

Maybe You can check isShown()? If TRUE - than it's user. Works for me.

setOnCheckedChangeListener(new OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (myCheckBox.isShown()) {// makes sure that this is shown first and user has clicked/dragged it
                  doSometing();
        }
    }
});
Envoi answered 3/2, 2012 at 14:16 Comment(4)
It works (meaning no unexpected callback) even you call "setChecked(isChecked)" in onStart() or onResume(). So it could be considered as a perfect solution.Unplaced
Seems like not a general solution. What if the button is shown in the moment but its value is changed from code?Rubric
I don't get it, how 'isShown()' is distinguish between user actions and programmatic changes? like how it can say it is a user action if 'isShown()' is true?Ptarmigan
This solution works only in very specific cases and relies on undocumented, non-guaranteed activity creation and layouting process. There is no guarantee this won't break in the future and it doesn't work if a view is already rendered.Gnni
B
28

You can remove the listener before changing it programatically and add it again, as answered in the following SO post:

https://mcmap.net/q/138149/-how-to-know-whether-user-has-changed-the-state-of-toggle-button

theCheck.setOnCheckedChangeListener(null);
theCheck.setChecked(false);
theCheck.setOnCheckedChangeListener(toggleButtonChangeListener);
Busra answered 6/7, 2015 at 15:13 Comment(0)
I
4

Try extending CheckBox. Something like that (not complete example):

public MyCheckBox extends CheckBox {

   private Boolean isCheckedProgramatically = false;

   public void setChecked(Boolean checked) {
       isCheckedProgramatically = true;
       super.setChecked(checked);
   }

   public Boolean isNotSetByUser() {
      return isCheckedProgramatically;
   }

}
Interlace answered 3/2, 2012 at 14:19 Comment(0)
T
4

Try NinjaSwitch:

Just call setChecked(boolean, true) to change the switch's checked state without detected!

public class NinjaSwitch extends SwitchCompat {

    private OnCheckedChangeListener mCheckedChangeListener;

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

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

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

    @Override
    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
        super.setOnCheckedChangeListener(listener);
        mCheckedChangeListener = listener;
    }

    /**
     * <p>Changes the checked state of this button.</p>
     *
     * @param checked true to check the button, false to uncheck it
     * @param isNinja true to change the state like a Ninja, makes no one knows about the change!
     */
    public void setChecked(boolean checked, boolean isNinja) {
        if (isNinja) {
            super.setOnCheckedChangeListener(null);
        }
        setChecked(checked);
        if (isNinja) {
            super.setOnCheckedChangeListener(mCheckedChangeListener);
        }
    }
}
Troup answered 14/1, 2016 at 13:50 Comment(0)
P
3

There is another simple solution that works pretty well. Example is for Switch.

public class BetterSwitch extends Switch {
  //Constructors here...

    private boolean mUserTriggered;

    // Use it in listener to check that listener is triggered by the user.
    public boolean isUserTriggered() {
        return mUserTriggered;
    }

    // Override this method to handle the case where user drags the switch
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean result;

        mUserTriggered = true;
        result = super.onTouchEvent(ev);
        mUserTriggered = false;

        return result;
    }

    // Override this method to handle the case where user clicks the switch
    @Override
    public boolean performClick() {
        boolean result;

        mUserTriggered = true;
        result = super.performClick();
        mUserTriggered = false;

        return result;
    }
}
Pigfish answered 26/8, 2014 at 17:41 Comment(0)
H
3

This should be enough :

SwitchCompact.setOnCheckedChangeListener((buttonView, isChecked) -> {
         if (buttonView.isPressed()) {
            if (!isChecked) {
               //do something
            } else {
              // do something else
            }
         }
      });
Hogweed answered 25/9, 2020 at 12:29 Comment(0)
I
2

Interesting question. To my knowledge, once you're in the listener, you can't detect what action has triggered the listener, the context is not enough. Unless you use an external boolean value as an indicator.

When you check the box "programmatically", set a boolean value before to indicate it was done programmatically. Something like:

private boolean boxWasCheckedProgrammatically = false;

....

// Programmatic change:
boxWasCheckedProgrammatically = true;
checkBoxe.setChecked(true)

And in your listener, don't forget to reset the state of the checkbox:

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    if (isNotSetByUser()) {
        resetBoxCheckSource();
        return;
    }
    doSometing();
}

// in your activity:
public boolean isNotSetByUser() {
    return boxWasCheckedProgrammatically;
}

public void resetBoxCheckedSource() {
    this.boxWasCheckedProgrammatically  = false;
}
Imhoff answered 3/2, 2012 at 14:18 Comment(0)
R
2

If OnClickListener is already set and shouldn't be overwritten, use !buttonView.isPressed() as isNotSetByUser().

Otherwise the best variant is to use OnClickListener instead of OnCheckedChangeListener.

Rubric answered 25/6, 2017 at 2:6 Comment(1)
buttonView.isPressed() is a nice solution. There is a problem when we use OnClickListener, when user slide on the switch, we will not get callback.Bugbear
R
2

The accepted answer could be simplified a bit to not maintain a reference to the original checkbox. This makes it so we can use the SilentSwitchCompat (or SilentCheckboxCompat if you prefer) directly in the XML. I also made it so you can set the OnCheckedChangeListener to null if you desire to do so.

public class SilentSwitchCompat extends SwitchCompat {
  private OnCheckedChangeListener listener = null;

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

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

  @Override
  public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
    super.setOnCheckedChangeListener(listener);
    this.listener = listener;
  }

  /**
   * Check the {@link SilentSwitchCompat}, without calling the {@code onCheckChangeListener}.
   *
   * @param checked whether this {@link SilentSwitchCompat} should be checked or not.
   */
  public void silentlySetChecked(boolean checked) {
    OnCheckedChangeListener tmpListener = listener;
    setOnCheckedChangeListener(null);
    setChecked(checked);
    setOnCheckedChangeListener(tmpListener);
  }
}

You can then use this directly in your XML like so (Note: you will need the whole package name):

<com.my.package.name.SilentCheckBox
      android:id="@+id/my_check_box"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textOff="@string/disabled"
      android:textOn="@string/enabled"/>

Then you can check the box silently by calling:

SilentCheckBox mySilentCheckBox = (SilentCheckBox) findViewById(R.id.my_check_box)
mySilentCheckBox.silentlySetChecked(someBoolean)
Roseannaroseanne answered 6/7, 2017 at 14:4 Comment(0)
H
2

Here is my implementation

Java Code for Custom Switch :

public class CustomSwitch extends SwitchCompat {

private OnCheckedChangeListener mListener = null;

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

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

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

@Override
public void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) {
    if(listener != null && this.mListener != listener) {
        this.mListener = listener;
    }
    super.setOnCheckedChangeListener(listener);
}

public void setCheckedSilently(boolean checked){
    this.setOnCheckedChangeListener(null);
    this.setChecked(checked);
    this.setOnCheckedChangeListener(mListener);
}}

Equivalent Kotlin Code :

class CustomSwitch : SwitchCompat {

private var mListener: CompoundButton.OnCheckedChangeListener? = null

constructor(context: Context) : super(context) {}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

override fun setOnCheckedChangeListener(@Nullable listener: CompoundButton.OnCheckedChangeListener?) {
    if (listener != null && this.mListener != listener) {
        this.mListener = listener
    }
    super.setOnCheckedChangeListener(listener)
}

fun setCheckedSilently(checked: Boolean) {
    this.setOnCheckedChangeListener(null)
    this.isChecked = checked
    this.setOnCheckedChangeListener(mListener)
}}

To change switch state without triggering listener use :

swSelection.setCheckedSilently(contact.isSelected)

You can monitor state change as normally by :

swSelection.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
   @Override
   public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
      // Do something
   }       
 });

In Kotlin :

 swSelection.setOnCheckedChangeListener{buttonView, isChecked -> run {
            contact.isSelected = isChecked
        }}
Heartwood answered 14/9, 2018 at 12:32 Comment(0)
R
1

My variant with Kotlin extension functions:

fun CheckBox.setCheckedSilently(isChecked: Boolean, onCheckedChangeListener: CompoundButton.OnCheckedChangeListener) {
    if (isChecked == this.isChecked) return
    this.setOnCheckedChangeListener(null)
    this.isChecked = isChecked
    this.setOnCheckedChangeListener(onCheckedChangeListener)
}

...unfortunately we need to pass onCheckedChangeListener every time because CheckBox class has not getter for mOnCheckedChangeListener field((

Usage:

checkbox.setCheckedSilently(true, myCheckboxListener)
Require answered 22/1, 2019 at 7:39 Comment(0)
P
0

Create a variable

boolean setByUser = false;  // Initially it is set programmatically


private void notSetByUser(boolean value) {
   setByUser = value;
}
// If user has changed it will be true, else false 
private boolean isNotSetByUser() {
   return setByUser;          

}

In the application when you change it instead of the user, call notSetByUser(true) so it is not set by the user, else call notSetByUser(false) i.e. it is set by program.

Lastly, in your event listener, after calling isNotSetByUser(), make sure you again change it back to normal.

Call this method whenever you are handling that action either thru user or programmatically. Call the notSetByUser() with appropriate value.

Pesek answered 3/2, 2012 at 14:21 Comment(0)
A
0

If the view's tag isn't used, you can use it instead of extending the checkbox:

        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                @Override
                public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) {
                    if (buttonView.getTag() != null) {
                        buttonView.setTag(null);
                        return;
                    }
                    //handle the checking/unchecking
                    }

each time you call something that checks/unchecks the checkbox, also call this before checking/unchecking :

checkbox.setTag(true);
Adjoin answered 10/6, 2014 at 11:48 Comment(0)
R
0

I have created extension with RxJava's PublishSubject, simple one. Reacts only on "OnClick" events.

/**
 * Creates ClickListener and sends switch state on each click
 */
fun CompoundButton.onCheckChangedByUser(): PublishSubject<Boolean> {
    val onCheckChangedByUser: PublishSubject<Boolean> = PublishSubject.create()
    setOnClickListener {
        onCheckChangedByUser.onNext(isChecked)
    }
    return onCheckChangedByUser
}
Runagate answered 1/3, 2019 at 8:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.