FlipFlopStyler.java

package swingtree;

import org.jspecify.annotations.Nullable;
import swingtree.animation.*;
import swingtree.api.AnimatedStyler;
import swingtree.style.ComponentStyleDelegate;

import javax.swing.JComponent;
import java.awt.event.ActionEvent;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 *  Models an animation which can switch between two states.
 *
 * @param <C> The type of the component which this flip-flop animation is applied to.
 * @author Daniel Nepp
 */
class FlipFlopStyler<C extends JComponent>
{
    private final LifeTime          _lifetime;
    private final AnimatedStyler<C> _styler;
    private final WeakReference<C>  _owner;

    private @Nullable AnimationState _state = null;
    private boolean _isOn = false;
    private boolean _isCurrentlyRunningAnimation = false;
    private @Nullable DisposableAnimation _animation = null;


    FlipFlopStyler( C owner, LifeTime lifetime, AnimatedStyler<C> styler ) {
        _owner     = new WeakReference<>(Objects.requireNonNull(owner));
        _lifetime  = Objects.requireNonNull(lifetime);
        _styler    = Objects.requireNonNull(styler);
    }


    ComponentStyleDelegate<C> style( ComponentStyleDelegate<C> delegate ) {
        AnimationState state = _state;
        if ( state == null )
            state = AnimationState.startOf(
                                LifeSpan.startingNowWith(_lifetime),
                                Stride.PROGRESSIVE,
                                new ActionEvent(this, 0, null)
                            );

        _state = state;
        return _styler.style(state, delegate);
    }

    void set( final boolean isOn ) {
        if ( _isOn == isOn ) return;
        C owner = _owner.get();
        if ( owner == null )
            return;

        LifeTime lifetime = _lifetime;
        long offset = 0;

        if ( _isCurrentlyRunningAnimation ) {
            if ( _animation != null ) {
                _animation.dispose();
                _animation = null;
            }
            /*
                Now this is tricky! We are in the middle of an animation transitioning between
                the on and off states. What we want is to start a new animation from the progress
                of the current animation. So we need to calculate the time offset for the new animation
                based on the progress of the current animation.
            */
            double progress = _state == null ? 0 : _state.progress();
            if ( _isOn )
                progress = 1 - progress;
            long animationDuration = lifetime.getDurationIn(TimeUnit.MILLISECONDS);
            offset = -(long) (animationDuration * progress);
        }

        _isCurrentlyRunningAnimation = true;

        _animation = new DisposableAnimation(new Animation() {
            @Override
            public void run( AnimationState state ) {
                _state = state;
                _isOn = isOn;
            }
            @Override
            public void finish( AnimationState state ) {
                _state = state;
                _isOn = isOn;
                _isCurrentlyRunningAnimation = false;
            }
        });
        Animator.animateFor(lifetime, isOn ? Stride.PROGRESSIVE : Stride.REGRESSIVE, owner)
                .goWithOffset(offset, TimeUnit.MILLISECONDS, _animation);
    }


    static class DisposableAnimation implements Animation
    {
        private @Nullable Animation _animation;

        DisposableAnimation( Animation animation ) {
            _animation = Objects.requireNonNull(animation);
        }

        @Override
        public void run(AnimationState state) {
            if ( _animation != null )
                _animation.run(state);
        }

        @Override
        public void finish(AnimationState state) {
            if ( _animation != null )
                _animation.finish(state);

            dispose();
        }

        public void dispose() {
            _animation = null;
        }
    }

}