JButton在Java Swing中悬停动画 [英] JButton hover animation in Java Swing

查看:115
本文介绍了JButton在Java Swing中悬停动画的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用Swing处理GUI窗口,我想创建一个navegetion菜单。我想用鼠标悬停在navegetion菜单的按钮上添加一个动画。



这样的事情:





我不能用鼠标监听器来做,因为它只是在没有动画的情况下在颜色之间切换





使用滚动需要GIF和不同的屏幕尺寸需要不同的gif大小。



有没有办法做一些抛出代码的方法?

解决方案

首先,你实际上是在问一个非常困难和复杂的问题。完成动画并不容易,需要大量的状态管理。



例如,如果用户在完成动画制作之前将鼠标移出菜单项会发生什么?你从现在的状态动画?你是从完成状态开始制作动画的吗?您是在整个时间范围内制作动画还是仅在剩余时间范围内制作动画?你将如何控制和管理它??



你也在询问有关颜色范围的动画。颜色混合实际上并不像听起来那么容易。当然你可能会说你只是动画阿尔法值,但混合颜色会给你一个更大的机会来提供不同的动画效果,比如从透明的绿色混合到不透明的蓝色,很酷。



动画通常也不是线性的,有很多理论可以用来制作动画,包括期待,压制恶臭,分期,等等,等等等等 - 老实说,有更好的人然后我带你通过那个。关键是,好的动画很复杂。



我的第一个建议是使用一个好的动画框架,比如

  import java.awt.Color; 
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
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.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;

公共类测试{

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);
}
});
}

公共类TestPane扩展JPanel {

public TestPane(){
setBorder(new EmptyBorder(8,8,8,8)) ;
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridwidth = GridBagConstraints.REMAINDER;

add(new MenuItem(Backup),gbc);
add(new MenuItem(Screenshots),gbc);
add(new MenuItem(Settings),gbc);

setBackground(Color.BLACK);
}

}

//对不起,我超过了字符数限制,你需要自己复制
//所有其​​他课程
}

您还可以查看:





你还应该看看 Swing中的并发性,以更好地理解引擎本身如何工作的基本概念以及为什么这样做?


I working on GUI window with Swing and I want to create a navegetion menu. I want to add an animation to the navegetion menu's buttons when hovering on them with the mouse.

Somthing like this:

I can't do it with mouse listener because it just change between colors without animation like so

With rolloving it require gifs and different screens sizes will require different gif sizes.

Is there a way to do it some how throw the code?

解决方案

First things first, you're actually asking a very difficult and complicated question. Animation done right is not easy and requires a lot state management.

For example, what happens if the user moves the mouse out of the menu item BEFORE it's finished animating? Do you animate from the current state? Do you animate from what was to be the finished state? Do you animate across the full time range or only the remaining time range? How would you control and manage it all!?

You're also asking about animating across color ranges. Color blending is actually not as easy as it might sound. Sure you could argue you're just animating the alpha value, but blending the colors gives you a much greater opportunity to provide a different animation affect, like blending from a transparent green to an opaque blue, cool.

Animation is also not normally linear, there is a lot of theory that goes into animating something well, including anticipation, squash an stench, staging, blah, blah, blah - honestly, there are better people then me to take you through that. The point is, good animation is complicated.

My first suggestion is to go an use a good animation framework, like TimingFramework which does all the heavy lifting for you.

On the off chance you can't do that, then you're in for a fun ride.

The first, well actually, easy thing, is to find a good color blending algorithm. I spent a lot of time exploring different algorithms and trying a bunch of different things before settling on something like...

protected Color blend(Color color1, Color color2, double ratio) {
    float r = (float) ratio;
    float ir = (float) 1.0 - r;

    float red = color1.getRed() * r + color2.getRed() * ir;
    float green = color1.getGreen() * r + color2.getGreen() * ir;
    float blue = color1.getBlue() * r + color2.getBlue() * ir;
    float alpha = color1.getAlpha() * r + color2.getAlpha() * ir;

    red = Math.min(255f, Math.max(0f, red));
    green = Math.min(255f, Math.max(0f, green));
    blue = Math.min(255f, Math.max(0f, blue));
    alpha = Math.min(255f, Math.max(0f, alpha));

    Color color = null;
    try {
        color = new Color((int) red, (int) green, (int) blue, (int) alpha);
    } catch (IllegalArgumentException exp) {
        exp.printStackTrace();
    }
    return color;
}

The reason I use this approach is I can easily move a color towards black or white, which many other approaches couldn't achieve (just moved the color to high or low ranges). This also focuses on blending two colors together based on a ratio of between 0 and 1 (so at 0.5, there balanced blend between the two).

Animation Engine...

Note: Swing is single threaded AND not thread safe, you need to take that into consideration when developing a solution. Okay, the hard part...

It's easy to do a linear animation, animation from the start of one point until you reach the end of another point. The problem with this is, they don't scale well and don't generally produce a good natural feeling animation.

Instead, what you want is a concept of an animation running over a period of time. From this you can calculate the progression through the given period and calculate the value to be applied.

This approach scales well, you can change the time and not care about anything else, it will take care of itself. It also works well on systems whose performance may not be up to a killer frame rate, as they can drop frames and mostly "fake" it

One key concept to keep in mind, animation is the "illusion" of change over time.

Range

Range is a simple generic representation of a starting and ending point, then given a normalised progression (0-1) can calculate a reasonable representation between those points.

public abstract 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 T valueAt(double progress);

}

The stupid thing is, I do this a lot, so, here is a helper class to encapsulate the core concept.

Animator

This is a "central" engine which drives all the animations. It repeatedly ticks at about every 5 milliseconds and drives the Animatable objects, which actually calculate the time through the animation.

public enum Animator {

    INSTANCE;

    private Timer timer;

    private List<Animatable> properies;

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

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

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

}

Animatable

This is "something" which can be animated. It has a concept of a Range and a Duration over which it should be animated (it also supports easement, but I will talk about that later)

public interface Animatable<T> {
    public Range<T> getRange();
    public T getValue();
    public boolean tick();
    public void setDuration(Duration duration);
    public Duration getDuration();
    public Easement getEasement();
}

AbstractAnimatable

A abstract implementation of Animatable. This does all the cool, awesome, calculations on each tick to determine how far through the given animation cycle it is and generates notifications to registered listeners about the change in state, so they can actually do something about it...

public abstract class AbstractAnimatable<T> implements Animatable<T> {

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

    private AnimatableListener<T> listener;

    private Easement easement;

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

        this.listener = listener;
    }

    public AbstractAnimatable(Range<T> range, Easement easement, AnimatableListener<T> listener) {
        this(range, listener);
        this.easement = easement;
    }

    public void setEasement(Easement easement) {
        this.easement = easement;
    }

    @Override
    public Easement getEasement() {
        return easement;
    }

    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;
    }

    public double getCurrentProgress(double rawProgress) {
        Easement easement = getEasement();
        double progress = Math.min(1.0, Math.max(0.0, getRawProgress()));
        if (easement != null) {
            progress = easement.interpolate(progress);
        }

        return progress;
    }

    public double getRawProgress() {
        if (startTime == null) {
            startTime = LocalDateTime.now();
        }
        Duration duration = getDuration();
        Duration runningTime = Duration.between(startTime, LocalDateTime.now());
        double progress = (runningTime.toMillis() / (double) duration.toMillis());
        return progress;
    }

    @Override
    public boolean tick() {
        double rawProgress = getRawProgress();
        double progress = getCurrentProgress(rawProgress);

        if (rawProgress >= 1.0) {
            progress = 1.0;
        }

        value = getRange().valueAt(progress);
        listener.stateChanged(this);

        return progress >= 1.0;
    }

}

AnimatableListener

A listener/observer to changes to a AbstractAnimatable state. This way, the AbstractAnimatable can tell interested parties that the state has been updated and what that state currently is.

public interface AnimatableListener<T> {

    public void stateChanged(Animatable<T> animator);
}

Easement

Okay, so I mentioned "animation theory". Basically what this is a implementation of a "spline interpolation" designed to provide common animation concepts, slow in, slow out, etc. What this does is changes the "progress" value through the animation so that the "speed" of the animation "appears" to change over the duration of the animation ... fancy, pancy for making it "look nice"

public enum Easement {

    SLOWINSLOWOUT(1d, 0d, 0d, 1d),
    FASTINSLOWOUT(0d, 0d, 1d, 1d),
    SLOWINFASTOUT(0d, 1d, 0d, 0d),
    SLOWIN(1d, 0d, 1d, 1d),
    SLOWOUT(0d, 0d, 0d, 1d);

    private final double points[];

    private final List<PointUnit> normalisedCurve;

    private Easement(double x1, double y1, double x2, double y2) {
        points = new double[]{x1, y1, x2, y2};

        final List<Double> baseLengths = new ArrayList<>();
        double prevX = 0;
        double prevY = 0;
        double cumulativeLength = 0;
        for (double t = 0; t <= 1; t += 0.01) {
            Point2D xy = getXY(t);
            double length = cumulativeLength
                            + Math.sqrt((xy.getX() - prevX) * (xy.getX() - prevX)
                                            + (xy.getY() - prevY) * (xy.getY() - prevY));

            baseLengths.add(length);
            cumulativeLength = length;
            prevX = xy.getX();
            prevY = xy.getY();
        }

        normalisedCurve = new ArrayList<>(baseLengths.size());
        int index = 0;
        for (double t = 0; t <= 1; t += 0.01) {
            double length = baseLengths.get(index++);
            double normalLength = length / cumulativeLength;
            normalisedCurve.add(new PointUnit(t, normalLength));
        }
    }

    public double interpolate(double fraction) {
        int low = 1;
        int high = normalisedCurve.size() - 1;
        int mid = 0;
        while (low <= high) {
            mid = (low + high) / 2;

            if (fraction > normalisedCurve.get(mid).getPoint()) {
                low = mid + 1;
            } else if (mid > 0 && fraction < normalisedCurve.get(mid - 1).getPoint()) {
                high = mid - 1;
            } else {
                break;
            }
        }
        /*
     * The answer lies between the "mid" item and its predecessor.
         */
        final PointUnit prevItem = normalisedCurve.get(mid - 1);
        final double prevFraction = prevItem.getPoint();
        final double prevT = prevItem.getDistance();

        final PointUnit item = normalisedCurve.get(mid);
        final double proportion = (fraction - prevFraction) / (item.getPoint() - prevFraction);
        final double interpolatedT = prevT + (proportion * (item.getDistance() - prevT));
        return getY(interpolatedT);
    }

    protected Point2D getXY(double t) {
        final double invT = 1 - t;
        final double b1 = 3 * t * invT * invT;
        final double b2 = 3 * t * t * invT;
        final double b3 = t * t * t;
        final Point2D xy = new Point2D.Double((b1 * points[0]) + (b2 * points[2]) + b3, (b1 * points[1]) + (b2 * points[3]) + b3);
        return xy;
    }

    protected double getY(double t) {
        final double invT = 1 - t;
        final double b1 = 3 * t * invT * invT;
        final double b2 = 3 * t * t * invT;
        final double b3 = t * t * t;
        return (b1 * points[2]) + (b2 * points[3]) + b3;
    }

    protected class PointUnit {

        private final double distance;
        private final double point;

        public PointUnit(double distance, double point) {
            this.distance = distance;
            this.point = point;
        }

        public double getDistance() {
            return distance;
        }

        public double getPoint() {
            return point;
        }

    }

}

So what?!

Okay, about now, you're probably scratching your head, wishing you hadn't asked the question ;)

How does all this help. The point is, all the above is re-usable, so you dump into a library somewhere and don't care about :/

What you do is then implement the required features and apply...

ColorRange

This takes the colorBlend algorithm from before and wraps into a Range concept...

public class ColorRange extends Range<Color> {

    public ColorRange(Color from, Color to) {
        super(from, to);
    }

    @Override
    public Color valueAt(double progress) {
        Color blend = blend(getTo(), getFrom(), progress);
        return blend;
    }

    protected Color blend(Color color1, Color color2, double ratio) {
        float r = (float) ratio;
        float ir = (float) 1.0 - r;

        float red = color1.getRed() * r + color2.getRed() * ir;
        float green = color1.getGreen() * r + color2.getGreen() * ir;
        float blue = color1.getBlue() * r + color2.getBlue() * ir;
        float alpha = color1.getAlpha() * r + color2.getAlpha() * ir;

        red = Math.min(255f, Math.max(0f, red));
        green = Math.min(255f, Math.max(0f, green));
        blue = Math.min(255f, Math.max(0f, blue));
        alpha = Math.min(255f, Math.max(0f, alpha));

        Color color = null;
        try {
            color = new Color((int) red, (int) green, (int) blue, (int) alpha);
        } catch (IllegalArgumentException exp) {
            exp.printStackTrace();
        }
        return color;
    }

}

This allows us to calculate the required color based on the progression through the animation

ColorAnimatable

Wh then make a "animatable color" concept...

public class ColorAnimatable extends AbstractAnimatable<Color> {

    public ColorAnimatable(ColorRange animationRange, Duration duration, AnimatableListener<Color> listener) {
        super(animationRange, listener);
        setDuration(duration);
    }

}

This means that we can establish a basic concept of a animation between two colors of a specified period of time and be notified when the animation state changes - it's nicely decoupled

Implementation...

And "finally" we're ready to actually make something out of it...

public class MenuItem extends JPanel {

    private Duration animationTime = Duration.ofSeconds(5);
    private JLabel label;

    private ColorAnimatable transitionAnimatable;

    private Color unfocusedColor = new Color(0, 0, 255, 0);
    private Color focusedColor = new Color(0, 0, 255, 255);

    public MenuItem() {
        setOpaque(false);
        setBorder(new EmptyBorder(8, 8, 8, 8));
        setLayout(new GridBagLayout());

        setBackground(unfocusedColor);

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1;
        gbc.fill = GridBagConstraints.BOTH;

        label = new JLabel();
        label.setForeground(Color.WHITE);
        label.setHorizontalAlignment(JLabel.LEADING);
        add(label, gbc);

        label.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                double progress = stopAnimation();
                transitionAnimatable = new ColorAnimatable(
                                new ColorRange(getBackground(), focusedColor), 
                                preferredAnimationTime(progress), 
                                new AnimatableListener<Color>() {
                    @Override
                    public void stateChanged(Animatable<Color> animator) {
                        setBackground(animator.getValue());
                    }
                });
                transitionAnimatable.setEasement(Easement.SLOWOUT);
                Animator.INSTANCE.add(transitionAnimatable);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                double progress = stopAnimation();
                transitionAnimatable = new ColorAnimatable(
                                new ColorRange(getBackground(), unfocusedColor), 
                                preferredAnimationTime(progress), 
                                new AnimatableListener<Color>() {
                    @Override
                    public void stateChanged(Animatable<Color> animator) {
                        setBackground(animator.getValue());
                    }
                });
                transitionAnimatable.setEasement(Easement.SLOWOUT);
                Animator.INSTANCE.add(transitionAnimatable);
            }

        });
    }

    public MenuItem(String text) {
        this();
        setText(text);
    }

    protected Duration preferredAnimationTime(double currentProgress) {
        if (currentProgress > 0.0 && currentProgress < 1.0) {
            double remainingProgress = 1.0 - currentProgress;
            double runningTime = animationTime.toMillis() * remainingProgress;
            return Duration.ofMillis((long)runningTime);
        } 

        return animationTime;
    }

    protected double stopAnimation() {
        if (transitionAnimatable != null) {
            Animator.INSTANCE.remove(transitionAnimatable);
            return transitionAnimatable.getRawProgress();
        }
        return 0.0;
    }

    public void setText(String text) {
        label.setText(text);
    }

    public String getText() {
        return label.getText();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        // Because we're faking it
        g.setColor(getBackground());
        g.fillRect(0, 0, getWidth(), getHeight());
    }

}

This looks complicated, but it's not to hard. Basically it defines a focused and unfocused color, sets up a MouseListener to monitor the mouseEntered and mouseExited events and setups the ColorAnimatable based on the required transition.

One thing this does do, which might not be obvious, is it will take into account unfinished animation. So if the user moves out of the item before the focus animation has completed, it will use the remaining time and the current color as starting point for the unfocus transition.

Because Swing components are either fully transparent or fully opaque, we need to "fake" the alpha support. We do this by making the component fully transparent and then painting the background ourselves

Runnable example...

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
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.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;

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() {
            setBorder(new EmptyBorder(8, 8, 8, 8));
            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.weightx = 1;
            gbc.fill = GridBagConstraints.HORIZONTAL;
            gbc.gridwidth = GridBagConstraints.REMAINDER;

            add(new MenuItem("Backup"), gbc);
            add(new MenuItem("Screenshots"), gbc);
            add(new MenuItem("Settings"), gbc);

            setBackground(Color.BLACK);
        }

    }

    // Sorry, I'm over the character limit, you will need to copy
    // all the other classes yourself
}

You could also have a look at:

You should also have a look at Concurrency in Swing to gain a better understanding into the underlying concept of how the engine itself works and why it was done this way

这篇关于JButton在Java Swing中悬停动画的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆