Change Switch state without animation
Asked Answered
C

8

45

In my Android project, I have a ListView with rows containing SwitchCompat items (AppCompat for Switch widget).

My problem occurs when I scroll into the list and getView(...) method of MyAdapter is invoked with a recycled view. I redefine the correct Switch state but the animation is visible.

There is a solution to prevent the animation in this case?

enter image description here

Cauthen answered 26/11, 2014 at 0:20 Comment(0)
B
82

Call jumpDrawablesToCurrentState() to skip the animation

switchCompat.setChecked(true);
switchCompat.jumpDrawablesToCurrentState();
Buckshee answered 8/10, 2015 at 16:57 Comment(3)
I've found this changes the color of the switch to be active, but it still remains in the 'unchecked' position..Favour
With data bindings, call executePendingBindings() before jumpDrawablesToCurrentState()Inflow
This works great for radio buttons / checkboxes if you are scrolling in a list.Ducan
C
10

I finally found a solution but seems not really clean:

ViewGroup viewGroup = (ViewGroup) view; // the recycled view
viewGroup.removeView(switch);
switch.setChecked(states[index]);
viewGroup.addView(switch);

If a better solution exists, please share it.

Cauthen answered 26/11, 2014 at 1:9 Comment(2)
Unfortunately your way appears to be the only one. Looking at the code for setChecked() in API 21 Switch: if (isAttachedToWindow() && isLaidOut()) { animateThumbToCheckedState(checked); }Erlene
@kcoppock, this code has been changed in SwitchCompat to if (isAttachedToWindow() && isLaidOut() && isShown()) so it is possible to hide it temporarily: switch.setVisibility(View.INVISIBLE); switch.setChecked(); switch.setVisibility(View.VISIBLE);Steapsin
C
4

The issue in with animation playing in the list can be present if you use Android Databinding.

To resolve it, run binding.executePendingBindings() method after you set data – it will refresh binding state for the component in current frame and will not wait for the next one to come.

As you have probably guessed already – next frame is the animation

Catechist answered 5/11, 2016 at 9:19 Comment(0)
T
3

I had the same problem and I managed to solved it using some minimal reflection.

Usage:

To change the switch state without animation call the setChecked(boolean checked, boolean animate) method with false for the animate parameter. If the switch already is animating at the moment this method is being called the animation will be stopped and the switch jumps to the desired position.

SwitchCompatFix.java

import android.content.Context;
import android.support.v7.widget.SwitchCompat;
import android.util.AttributeSet;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Work around for: https://mcmap.net/q/367246/-change-switch-state-without-animation
 * Possible fix for bug 101107: https://code.google.com/p/android/issues/detail?id=101107
 *
 * Version 0.2
 * @author Rolf Smit
 */
public class SwitchCompatFix extends SwitchCompat {

    public SwitchCompatFix(Context context) {
        super(context);
        initHack();
    }

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

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

    private Method methodCancelPositionAnimator = null;
    private Method methodSetThumbPosition = null;

    private void initHack(){
        try {
            methodCancelPositionAnimator = SwitchCompat.class.getDeclaredMethod("cancelPositionAnimator");
            methodSetThumbPosition = SwitchCompat.class.getDeclaredMethod("setThumbPosition", float.class);
            methodCancelPositionAnimator.setAccessible(true);
            methodSetThumbPosition.setAccessible(true);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public void setChecked(boolean checked, boolean animate){
        // Java does not support super.super.xxx calls, a call to the SwitchCompat default setChecked method is needed.
        super.setChecked(checked);
        if(!animate) {

            // See original SwitchCompat source:
            // Calling the super method may result in setChecked() getting called
            // recursively with a different value, so load the REAL value...
            checked = isChecked();

            // Cancel any running animations (started by super.setChecked()) and immediately move the thumb to the new position
            try {
                if(methodCancelPositionAnimator != null && methodSetThumbPosition != null) {
                    methodCancelPositionAnimator.invoke(this);
                    methodSetThumbPosition.invoke(this, checked ? 1 : 0);
                }
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
}

Note for proguard users:

Because this method uses reflection an additional proguard rule might be needed (if not yet present).

-keep class android.support.v7.widget.SwitchCompat {
    private void cancelPositionAnimator();
    private void setThumbPosition(float);
}

This additional rule is not needed when you're using one of the following proguard rules (or similar ones):

-keep class android.support.v7.widget.** { *; }
-keep class android.support.v7.** { *; }
Tilth answered 13/4, 2015 at 15:7 Comment(2)
May I ask if you know why in some cases it actually skips the animation of being checked/unchecked ? I've made a library for supporting material design preferences (here: github.com/AndroidDeveloperLB/MaterialPreferenceLibrary ), and I have no idea why it skips the animation and switches directly to the toggled state.Zakaria
oh oh reflectionLareelareena
D
1

Using SwitchCompat and DataBinding

@BindingAdapter({"bind:checkedState"})
public static void setCheckedState(SwitchCompat switchView, boolean checked) {
    int visibility = switchView.getVisibility();
    switchView.setVisibility(View.INVISIBLE);
    switchView.setChecked(checked);
    switchView.setVisibility(visibility);
}

Then in xml:

<android.support.v7.widget.SwitchCompat
    android:id="@+id/my_switch"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:checkedState="@{my_data.checked}"/>

And don't forget to call executePendingBindings() (thanks AAverin)

Durazzo answered 2/10, 2016 at 7:55 Comment(1)
Also consider using binding.executePendingBindings() for list elements, because this is the main reason this animation runs on screen – one extra frame of executionCatechist
U
0

For Kotlin developer:

fun SwitchCompat.setCheckedWithoutAnimation(checked: Boolean) {
    val beforeVisibility = visibility
    visibility = View.INVISIBLE
    isChecked = checked
    visibility = beforeVisibility
}

And the usage:

mySwitch.setCheckedWithoutAnimation(true)
Untwist answered 8/9, 2016 at 11:23 Comment(2)
Would you mind explaining why this skips the animation? Because if you look at the code of SwitchCompat#setChecked the visibility has no effect on the animation.Carpophore
Depending what LayoutTransition you have enabled on your parent layout, I think this might still animate, but differently. It should animate from invisible to visible :)Phlebosclerosis
F
0

Kotlin sample of Rolf ツ answer.

class SwitchImproved(context: Context, attributeSet: AttributeSet) : SwitchCompat(context, attributeSet) {

    private lateinit var methodCancelPositionAnimator: Method
    private lateinit var methodSetThumbPosition: Method

    init {
        initHack()
    }

    fun setChecked(checked: Boolean, animate: Boolean = true) {
        super.setChecked(checked)

        if (!animate) {
            methodCancelPositionAnimator.invoke(this)
            methodSetThumbPosition.invoke(this, if (isChecked) 1 else 0)
        }
    }

    private fun initHack() {
        methodCancelPositionAnimator = SwitchCompat::class.java.getDeclaredMethod("cancelPositionAnimator")
        methodSetThumbPosition = SwitchCompat::class.java.getDeclaredMethod("setThumbPosition", Float::class.javaPrimitiveType)
        methodCancelPositionAnimator.isAccessible = true
        methodSetThumbPosition.isAccessible = true
    }
}
Firepower answered 14/1, 2021 at 10:16 Comment(0)
J
-1

In my case, I am using the new material library:

implementation 'com.google.android.material:material:1.1.0-alpha07'

and in the setChecked method of this class there is this condition:

if (getWindowToken() != null && ViewCompat.isLaidOut(this))

So what I did was to create a class that extends from this SwitchMaterial, and deal with "isLaidOut". The code is the next one (omitting constructors):

class SwitchCustomView : SwitchMaterial {

  private var laidOutForAnimation = false

  fun setChecked(checked: Boolean, animate: Boolean) {
    if (!animate) {
      laidOutForAnimation = true
    }
    super.setChecked(checked)
    laidOutForAnimation = false
  }

  override fun isLaidOut(): Boolean {
    return if (laidOutForAnimation) {
      return false
    } else {
      super.isLaidOut()
    }
  }
}

Then just use this class in your xml and call programatically

setChecked(checked: Boolean, animate: Boolean)
Jodyjoe answered 23/6, 2019 at 12:38 Comment(1)
@Lareelareena at the moment I am using com.google.android.material:material:1.1.0-beta02 and it is working just fine for me. There may be many reasons why it is not working for you. Sadly, I can't help you with the information you are giving.Jodyjoe

© 2022 - 2024 — McMap. All rights reserved.