Button.setStateListAnimator() overwrites ObjectAnimator's target object with the button widget

animation

#1

Hi guys, I’ trying to animate the background color of a button. I’m struggling with this issue, probably I don’t understand everything yet.

    var btn = args.object.parent.getViewById("xxx");
    var btnNative = btn.nativeView;

    var s = new android.graphics.drawable.GradientDrawable();
    s.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
    s.setCornerRadius(10);

    var animList = new android.animation.StateListAnimator();
    animList.addState([android.R.attr.state_pressed], 
        android.animation.ObjectAnimator.ofArgb(s,"color",[0xFF00FFFF]).setDuration(1000)
    );
    animList.addState([0], 
        android.animation.ObjectAnimator.ofArgb(s,"color",[0xFF00FF00]).setDuration(1000)
    );
    
    btnNative.setBackground(s);
    btnNative.setStateListAnimator(animList);

When the button is pushed, the background should change color in response but it does nothing. The same code works well when I change the "color" for eg. "left".

However when I use ValueAnimator instead of ObjectAnimator, it works.

    var listener = new android.animation.ValueAnimator.AnimatorUpdateListener({
        onAnimationUpdate: function(animation) {
            s.setColor(animation.getAnimatedValue().intValue());
        },
    });

    var anim1 = android.animation.ValueAnimator.ofArgb([0xFF00FFFF, 0xFF00FF00]);
    anim1.setDuration(1000);
    anim1.addUpdateListener(listener);

    var anim2 = android.animation.ValueAnimator.ofArgb([0xFF00FF00, 0xFF00FFFF]);
    anim2.setDuration(1000);
    anim2.addUpdateListener(listener);

    var animList = new android.animation.StateListAnimator();
    animList.addState([android.R.attr.state_pressed], 
        anim1
    );
    animList.addState([0], 
        anim2
    );

However I don’t like this solution because of the flicker when the tap is released before the animation ends. It cannot continue with the current color value when the previous animation is cancelled.

I was also trying with "alpha" which neither worked. This question&answer suggests me that is should.


#2

Update

This code works without problem:

android.animation.ObjectAnimator.ofArgb(s,"color",[0xFF888888]).setDuration(1000).start();

So the problem is with the StateListAnimator or the Button.setStateListAnimator() or somwhere else.


#3

Weird update

I’m trying this code:

    var ColorProp = android.util.IntProperty.extend({
        init: function() {},
        set: function(obj, value) {
            obj.setColor(value);    // <- breakpoint
        },
        setValue: function(obj, value) {
            obj.setColor(value);    // <- breakpoint
        },
        get: function(obj) {
            return obj.getColor();    // <- breakpoint
        },
    });

    var colorProp = new ColorProp("color");

    var animList = new android.animation.StateListAnimator();
    animList.addState([android.R.attr.state_pressed], 
        android.animation.ObjectAnimator.ofArgb(s,colorProp,[200]).setDuration(1000)
    );
    animList.addState([0], 
        android.animation.ObjectAnimator.ofArgb(s,colorProp,[0]).setDuration(1000)
    );

    btnNative.setBackground(s);
    btnNative.setStateListAnimator(animList);

I place breakpoints on the indicated lines. The setValue and get functions get called. In these functions

obj.toString()

returns

"android.widget.Button{498f4ef VFED..C.. ......ID 32,114-1168,210}"

instead of android.graphics.drawable.GradientDrawable, as s is a GradientDrawable! This is the weird thing.



When I run

android.animation.ObjectAnimator.ofArgb(s,colorProp,[0xFF888888]).setDuration(1000).start();

at the breakpoints

obj.toString()

gives

"android.graphics.drawable.GradientDrawable@960327b"

as expected.

I haven’t found anything in the Android docs so far. I don’t know if this problem is NativeScript related but I cannot try it in Java because I have no experience with Java. I’d appreciate if somebody could try this. According to this Q&A it should work.


#4

This issue is not related to NativeScript. I confirmed this phenomenon in Android in StateListAnimator.java source, StateListAnimator manipulates the animation target object calling setTarget(target) in different places.

I wonder that nobody encountered this limitation before.

So far I found one possible workaround: to animate backgroundTint or foregroundTint of the Button:

var myButton = view.getViewById(page, "xxx");
var myButtonNative = myButton.nativeView;

var TintProp = android.util.IntProperty.extend({
    set: function(obj, integerObj) {
        this.setValue(obj, integerObj.intValue());
    },
    setValue: function(obj, value) {
        obj.setBackgroundTintList(android.content.res.ColorStateList.valueOf(value));
    },
    get: function(obj) {
        return new java.lang.Integer(this.getValue(obj));
    },
    getValue: function(obj) {
        var c = obj &&
                obj.getBackgroundTintList && 
                obj.getBackgroundTintList() && 
                obj.getBackgroundTintList().getDefaultColor();
        return c ? c : 0;
    },
});

var tintProp = new TintProp("tint");

var animList = new android.animation.StateListAnimator();
animList.addState([android.R.attr.state_pressed], 
    android.animation.ObjectAnimator.ofArgb(myButtonNative,tintProp,[0x440000FF]).setDuration(50)
);
animList.addState([0], 
    android.animation.ObjectAnimator.ofArgb(myButtonNative,tintProp,[0x000000FF]).setDuration(500)
);

myButtonNative.setBackgroundTintMode(android.graphics.PorterDuff.Mode.SRC_OVER);
myButtonNative.setStateListAnimator(animList);

This workaround is somehow limited because it can draw only a solid color over the whole existing background.