Sliding effect menu with JComponents

2019-08-01 10:13发布

问题:

I'm trying to make some sliding effect with JLabels and timers. I'd like to use only two timers (IN and OUT) to manage the effect for multiple components. The problem is that everything works fine only if I don't move quickly from one JLabel to another and I don't know how to manage the thing.

Gif showing the problem

Here is my code:

public class Sliders extends JFrame {

private JPanel contentPane;
JLabel label,label_1;
static RainDrop frame;
 javax.swing.Timer  in,out;
/**
 * Launch the application.
 */
public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
        public void run() {
            try {
                frame = new RainDrop();
                frame.setVisible(true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

/**
 * Create the frame.
 */
public Sliders() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setBounds(100, 100, 450, 300);
    contentPane = new JPanel();
    contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
    setContentPane(contentPane);
    contentPane.setLayout(null);

    label = new JLabel("");
    label.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseEntered(MouseEvent a1) {
            setIN(2,0,label);
                System.out.println("ENTRATO");
                checkloop_out_mag(-270,label);

        }
        @Override
        public void mouseExited(MouseEvent a2) {
            in.stop();
            setOUT(-2,0,label);
            System.out.println("USCITO");                       
        }
    });
    label.setBackground(Color.ORANGE);
    label.setOpaque(true);
    label.setBounds(-270, 0, 337, 44);
    contentPane.add(label);

    label_1 = new JLabel("");
    label_1.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseEntered(MouseEvent b1) {
                setIN(2,44,label_1);
                System.out.println("ENTRATO");
                checkloop_out_mag(-270,label_1);
        }
        @Override
        public void mouseExited(MouseEvent b2) {
            in.stop();
            setOUT(-2,44,label_1);
            System.out.println("USCITO");                   
        }
    });
    label_1.setOpaque(true);
    label_1.setBackground(Color.GREEN);
    label_1.setBounds(-270, 44, 337, 44);
    contentPane.add(label_1);
}



public void setIN(int x,int y,JLabel label) {


    in = new javax.swing.Timer(2, new ActionListener() {


                @Override
                public void actionPerformed(ActionEvent e) {



                     label.setLocation(label.getBounds().x+x,y);

                     System.out.println("SPOSTO");

                     System.out.println("CONTROLLO");
                     checkloop_in_magequals(0,label); 

                }
            });
            in.setRepeats(true);
            in.start();



}

public void setOUT(int x,int y,JLabel label) {


    out = new javax.swing.Timer(2, new ActionListener() {


                @Override
                public void actionPerformed(ActionEvent e) {



                     label.setLocation(label.getBounds().x+x,y);

                     System.out.println("SPOSTO");

                     System.out.println("CONTROLLO");
                       checkloop_out_equals(-270,label);


                }
            });
            out.setRepeats(true);
            out.start();



}

public void checkloop_out_equals(int z,JLabel label) {
     if (label.getBounds().x==z){
            out.stop();
            System.out.println("STOP");
     }
}

public void checkloop_out_mag(int z,JLabel label) {
     if (label.getBounds().x>z){
            out.stop();
            System.out.println("STOP");
     }
}


public void checkloop_in_magequals(int z,JLabel label) {
 if (label.getBounds().x>=z){
        in.stop();
        System.out.println("STOP");
     }
 }
}

Is there a way to fix the code using only two timers? Or do I need two timers for each JComponent?

回答1:

First of all, go use a animation framework, like Trident or The Timing Framework or The Universal Tween Engine, they provide an awesome amount of functionality which would otherwise take an awful lot of coding to accomplish.

Animation is a complex subject, so I'm only going to cover some primary basics. Animation is the illusion of change over time (told you it was going to be basic).

The crust of your you problem comes down to two basic things:

  1. You need a central "clock" which can provide "ticks" and (a relative) regular interval
  2. A API which agnostic enough, decoupled enough, that it doesn't care "what" it's animating, only that, given a duration and range, it can calculate the required properties on each "tick"

One important concept to get your head around is, animation should played over a period of time, not between values. The main reason for this is flexibility. It's much easier to change the duration of the animation in order to change the speed and it allows the system to "drop" frames relatively simply. Remember when I said "Animation is the illusion of change over time" - this is what I'm talking about.

Simple ...

Let's start with some basics...

public class Range<T> {
    private T from;
    private T to;

    public Range(T from, T to) {
        this.from = from;
        this.to = to;
    }

    public T getFrom() {
        return from;
    }

    public T getTo() {
        return to;
    }

    @Override
    public String toString() {
        return "From " + getFrom() + " to " + getTo();
    }

}

Range describes something which can be measured in terms of a "start" (from) value and a "target" (to) value. This allows use to supply a number of important values, but most notably, the "range" of the animation we want to execute.

In the past I've used such a concept for all sorts of values, including Point, Rectangle and even Color

public interface AnimationPropertiesListener<T> {
    public void stateChanged(AnimationProperties<T> animator);
}

public interface AnimationProperties<T> {
    public Range<T> getRange();
    public T getValue();
    public boolean tick();

    public void setDuration(Duration duration);
    public Duration getDuration();
}

public abstract class AbstractAnimationProperties<T> implements AnimationProperties<T> {

    private Range<T> range;     
    private LocalDateTime startTime;
    private Duration duration = Duration.ofSeconds(5);
    private T value;
    private AnimationPropertiesListener<T> listener;

    public AbstractAnimationProperties(Range<T> range, AnimationPropertiesListener<T> listener) {
        this.range = range;
        this.value = range.getFrom();

        this.listener = listener;
    }

    public void setDuration(Duration duration) {
        this.duration = duration;
    }

    public Duration getDuration() {
        return duration;
    }

    public Range<T> getRange() {
        return range;
    }

    @Override
    public T getValue() {
        return value;
    }

    @Override
    public boolean tick() {
        if (startTime == null) {
            startTime = LocalDateTime.now();
        }
        Duration duration = getDuration();
        Duration runningTime = Duration.between(startTime, LocalDateTime.now());
        Duration timeRemaining = duration.minus(runningTime);
        if (timeRemaining.isNegative()) {
            runningTime = duration;
        }
        double progress = (runningTime.toMillis() / (double) duration.toMillis());
        value = calculateValue(progress);

        listener.stateChanged(this);

        return progress >= 1.0;
    }

    public abstract T calculateValue(double progress);

}

The "animation properties" is the underlying power of what we are trying to achieve. It's responsible for calculate the amount of time the animation wants to run for, the amount of time the animation has been running and provides a means to calculate the resulting value for the specified animation Range value. (note the calculateValue could be a property of the Range, but I just left it where it was)

Okay, that's all fine and good, but we actually need a central clock to provide all the ticks and notify the properties

public enum Animator {

    INSTANCE;

    private Timer timer;

    private List<AnimationProperties> properies;

    private Animator() {
        properies = new ArrayList<>(5);
        timer = new Timer(5, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                Iterator<AnimationProperties> it = properies.iterator();
                while (it.hasNext()) {
                    AnimationProperties ap = it.next();
                    if (ap.tick()) {
                        it.remove();
                    }
                }
                if (properies.isEmpty()) {
                    timer.stop();
                }
            }
        });
    }

    public void add(AnimationProperties ap) {
        properies.add(ap);
        timer.start();
    }

    public void remove(AnimationProperties ap) {
        properies.remove(ap);
        if (properies.isEmpty()) {
            timer.stop();
        }
    }

}

Okay, so this is a really simple implementation. It is just a Swing Timer which runs really fast. When there AnimationProperties available, it continues looping until all the AnimationProperties tick method return true (completed) at which time it stops (so it's not doing unless things in the background)

Okay, but how does this all help us?

Well, basically, all we want to do is calculate the new width of the component from a given value to a given value over a period of time. Since width is defined as a int, we can create a series of concrete classes based on the above abstract/generic class, for example...

public class IntRange extends Range<Integer> {

    public IntRange(Integer from, Integer to) {
        super(from, to);
    }

    public Integer getDistance() {
        return getTo() - getFrom();
    }
}

public class IntAnimationProperties extends AbstractAnimationProperties<Integer> {

    public IntAnimationProperties(IntRange animationRange, IntRange maxRange, Duration duration, AnimationPropertiesListener<Integer> listener) {
        super(animationRange, listener);

        int maxDistance = maxRange.getDistance();
        int aniDistance = animationRange.getDistance();

        double progress = Math.min(100, Math.max(0, Math.abs(aniDistance/ (double)maxDistance)));
        Duration remainingDuration = Duration.ofMillis((long)(duration.toMillis() * progress));
        setDuration(remainingDuration);
    }

    @Override
    public Integer calculateValue(double progress) {
        IntRange range = (IntRange)getRange();
        int distance = range.getDistance();
        int value = (int) Math.round((double) distance * progress);
        value += range.getFrom();
        return value;
    }

}

What's really important to note here is, the IntAnimationProperties also requires a "max range" value. This is the total available range that the animation would ever be expected to run over. This is used to calculate the current "progress" value that the "animation range" is at.

Consider what would happen if the one panel was half expanded when the user exited it? Normally it would have to use the whole duration to animate back that half range.

Instead, this implementation calculates the "remaining" duration required to move to the desired point, in the example above, that's half the normal duration.

Example...

So, based on the above, we could end up with something like...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(null);
            Slider slider1 = new Slider();
            slider1.setBackground(Color.BLUE);
            slider1.setLocation(0, 44);         
            add(slider1);

            Slider slider2 = new Slider();
            slider2.setBackground(Color.MAGENTA);
            slider2.setLocation(0, 88);
            add(slider2);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }

    public class Slider extends JPanel {

        private AnimationProperties<Integer> ap;

        private IntRange maxRange = new IntRange(44, 150);
        private Duration duration = Duration.ofSeconds(5);

        public Slider() {
            setSize(44, 44);

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    animateTo(150);
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    animateTo(44);
                }

                public void animateTo(int to) {
                    if (ap != null) {
                        Animator.INSTANCE.remove(ap);
                    }
                    IntRange animationRange = new IntRange(getWidth(), to);
                    ap = new IntAnimationProperties(animationRange, maxRange, duration, new AnimationPropertiesListener<Integer>() {
                        @Override
                        public void stateChanged(AnimationProperties<Integer> animator) {
                            setSize(animator.getValue(), 44);
                            repaint();
                        }
                    });
                    Animator.INSTANCE.add(ap);
                }

            });
        }

    }

    public enum Animator {

        INSTANCE;

        private Timer timer;

        private List<AnimationProperties> properies;

        private Animator() {
            properies = new ArrayList<>(5);
            timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    Iterator<AnimationProperties> it = properies.iterator();
                    while (it.hasNext()) {
                        AnimationProperties ap = it.next();
                        if (ap.tick()) {
                            it.remove();
                        }
                    }
                    if (properies.isEmpty()) {
                        timer.stop();
                    }
                }
            });
        }

        public void add(AnimationProperties ap) {
            properies.add(ap);
            timer.start();
        }

        public void remove(AnimationProperties ap) {
            properies.remove(ap);
            if (properies.isEmpty()) {
                timer.stop();
            }
        }

    }

    public interface AnimationProperties<T> {
        public Range<T> getRange();
        public T getValue();
        public boolean tick();

        public void setDuration(Duration duration);
        public Duration getDuration();
    }

    public interface AnimationPropertiesListener<T> {
        public void stateChanged(AnimationProperties<T> animator);
    }

    public class Range<T> {
        private T from;
        private T to;

        public Range(T from, T to) {
            this.from = from;
            this.to = to;
        }

        public T getFrom() {
            return from;
        }

        public T getTo() {
            return to;
        }

        @Override
        public String toString() {
            return "From " + getFrom() + " to " + getTo();
        }

    }

    public abstract class AbstractAnimationProperties<T> implements AnimationProperties<T> {

        private Range<T> range;

        private LocalDateTime startTime;

        private Duration duration = Duration.ofSeconds(5);

        private T value;

        private AnimationPropertiesListener<T> listener;

        public AbstractAnimationProperties(Range<T> range, AnimationPropertiesListener<T> listener) {
            this.range = range;
            this.value = range.getFrom();

            this.listener = listener;
        }

        public void setDuration(Duration duration) {
            this.duration = duration;
        }

        public Duration getDuration() {
            return duration;
        }

        public Range<T> getRange() {
            return range;
        }

        @Override
        public T getValue() {
            return value;
        }

        @Override
        public boolean tick() {
            if (startTime == null) {
                startTime = LocalDateTime.now();
            }
            Duration duration = getDuration();
            Duration runningTime = Duration.between(startTime, LocalDateTime.now());
            Duration timeRemaining = duration.minus(runningTime);
            if (timeRemaining.isNegative()) {
                runningTime = duration;
            }
            double progress = (runningTime.toMillis() / (double) duration.toMillis());
            value = calculateValue(progress);

            listener.stateChanged(this);

            return progress >= 1.0;
        }

        public abstract T calculateValue(double progress);

    }

    public class IntRange extends Range<Integer> {

        public IntRange(Integer from, Integer to) {
            super(from, to);
        }

        public Integer getDistance() {
            return getTo() - getFrom();
        }
    }

    public class IntAnimationProperties extends AbstractAnimationProperties<Integer> {

        public IntAnimationProperties(IntRange animationRange, IntRange maxRange, Duration duration, AnimationPropertiesListener<Integer> listener) {
            super(animationRange, listener);

            int maxDistance = maxRange.getDistance();
            int aniDistance = animationRange.getDistance();

            double progress = Math.min(100, Math.max(0, Math.abs(aniDistance/ (double)maxDistance)));
            Duration remainingDuration = Duration.ofMillis((long)(duration.toMillis() * progress));
            setDuration(remainingDuration);
        }

        @Override
        public Integer calculateValue(double progress) {
            IntRange range = (IntRange)getRange();
            int distance = range.getDistance();
            int value = (int) Math.round((double) distance * progress);
            value += range.getFrom();
            return value;
        }

    }

}

But that's really slow ☹️

Okay, two places you change the duration. AbstractAnimationProperties has a default Duration of 5 seconds and Slider has a default Duration of 5 seconds. In your case, changing the Sliders Duration is probably where you want to start

Oh, wait, you want "easement" (slow in/slow out) to? Well, go have a look at How can I implement easing functions with a thread and then go have a look at the animation frameworks I linked earlier, because they already do this

Now if you're really intent on doing this by hand - you could have a look at this gist which is a "time line" based implementation which supports easement