JavaSwing中的JButton悬停动画

JavaSwing中的JButton悬停动画,java,swing,user-interface,Java,Swing,User Interface,我正在使用Swing处理GUI窗口,我想创建一个导航菜单。当鼠标悬停在导航菜单的按钮上时,我想向它们添加动画 像这样的事情: 我不能用鼠标监听器,因为它只是在没有动画的情况下在颜色之间切换 滚动需要gif,不同的屏幕大小需要不同的gif大小 有没有一种方法可以做到这一点一些如何抛出代码?首先,你实际上在问一个非常困难和复杂的问题。正确地制作动画并不容易,需要大量的状态管理 例如,如果用户在完成动画制作之前将鼠标移出菜单项,会发生什么情况?是否从当前状态设置动画?您是否从即将完成的状态设置动画

我正在使用Swing处理GUI窗口,我想创建一个导航菜单。当鼠标悬停在导航菜单的按钮上时,我想向它们添加动画

像这样的事情:

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

滚动需要gif,不同的屏幕大小需要不同的gif大小


有没有一种方法可以做到这一点一些如何抛出代码?

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

例如,如果用户在完成动画制作之前将鼠标移出菜单项,会发生什么情况?是否从当前状态设置动画?您是否从即将完成的状态设置动画?您是在整个时间范围内设置动画,还是仅在剩余时间范围内设置动画?你将如何控制和管理这一切

您还询问了有关跨颜色范围设置动画的问题。颜色混合实际上并不像听起来那么容易。当然,你可能会争辩说你只是在设置alpha值的动画,但是混合颜色给了你提供不同动画效果的更多机会,就像从透明的绿色混合到不透明的蓝色,很酷

动画通常也不是线性的,有很多理论可以很好地制作动画,包括预测、压扁臭味、舞台表演等等,诸如此类,诸如此类——老实说,有比我更好的人可以带你经历这些。关键是,好的动画是复杂的

我的第一个建议是使用一个好的动画框架,就像它为你做所有的繁重工作一样

万一你做不到这一点,那你就要去玩了

首先,其实很简单,就是找到一个好的颜色混合算法。我花了很多时间探索不同的算法,尝试了很多不同的东西,然后决定做一些像

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;
}
我使用这种方法的原因是我可以很容易地将颜色移向黑色或白色,这是许多其他方法无法实现的(只是将颜色移到高或低范围)。这还侧重于根据
0
1
之间的比率将两种颜色混合在一起(因此在
0.5
时,两者之间存在平衡的混合)

动画引擎。。。 注意:Swing是单线程的,不是线程安全的,在开发解决方案时需要考虑到这一点。 好吧,最难的部分

制作线性动画很容易,动画从一个点的开始一直到另一个点的结束。问题是,它们不能很好地缩放,通常不能产生良好的自然感动画

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

}
相反,您需要的是动画在一段时间内运行的概念。由此,您可以计算给定期间的进度,并计算要应用的值

这种方法可以很好地扩展,您可以改变时间,而不必关心其他任何事情,它会自行处理。它也适用于性能可能达不到致命帧速率的系统,因为它们可以丢弃帧,并且大多是“伪造”帧

要记住的一个关键概念是,动画是随时间变化的“幻觉”

范围
Range
是起点和终点的简单通用表示,然后给定一个标准化的级数(
0-1
)可以计算出这些点之间的合理表示

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

}
Animatable
这是可以设置动画的“东西”。它有一个
范围
持续时间
的概念,在这个概念上它应该被设置动画(它也支持地役权,但我将在后面讨论)

AnimatableListener
侦听器/观察者更改为
可抽象动画
状态。通过这种方式,
AbstractAnimatable
可以告诉相关方状态已经更新,以及当前状态是什么

public interface AnimatableListener<T> {

    public void stateChanged(Animatable<T> animator);
}
那又怎么样?! 好吧,现在,你可能在挠头,希望你没有问这个问题;)

这一切有什么帮助。关键是,以上所有内容都是可重用的,因此您可以将其转储到某个库中,而不必关心:/

然后,您要做的是实现所需的功能并应用

ColorRange
这将采用以前的
colorBlend
算法,并将其包装成
范围
概念

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

}
public class ColorAnimatable extends AbstractAnimatable<Color> {

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

}
这意味着我们可以在指定时间段的两种颜色之间建立动画的基本概念,并在动画状态发生变化时得到通知-这是很好的解耦

实施 “最后”我们已经准备好从中得到一些东西

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

}
您还可以看看:

  • (尚未使用)
  • (上面的很多内容都是松散地基于此)
  • (未使用)是框架的一部分

您还应该看一看,以便更好地理解引擎本身如何工作的基本概念,以及为什么这样做

这是一个比MadProgrammer建议的简单得多的实现。它做出了一些天真的假设,例如线性颜色插值。它也不会尝试动画颜色的alpha混合。而且它不支持响应键盘导航的动画

内部音量控制器类使用javax.swing.Timer沿一个方向执行动画。每个JMenuItem都指定了两个音量控制器对象,一个用于转换为高光颜色,另一个用于转换回常规背景颜色

还有必要重写UI委托对象,因为大多数外观和感觉都希望强制使用自己的颜色,而不管菜单项的背景是什么,无论是在任何时候还是在菜单项具有键盘焦点时

import java.util.Objects;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.Timer;
import javax.swing.plaf.basic.BasicMenuItemUI;

public class FadingMenuTest {
    private static final int animDurationMillis = 500;
    private static final int frameCount = 8;

    private static class Fader {
        private final Component component;

        private final Color normalBackground;
        private final float[] normalRGBA;
        private final float[] targetRGBA;
        private final float[] rgba = new float[4];

        private final Timer timer = new Timer(animDurationMillis / frameCount,
            e -> updateBackground());

        private int frameNumber;

        Fader(Component component,
              Color targetBackground,
              Color normalBackground) {

            this.component = Objects.requireNonNull(component);
            this.normalBackground = normalBackground;
            this.normalRGBA = normalBackground.getComponents(null);
            this.targetRGBA = targetBackground.getComponents(null);
        }

        private void updateBackground() {
            if (++frameNumber > frameCount) {
                timer.stop();
                return;
            }

            for (int i = rgba.length - 1; i >= 0; i--) {
                float normal = normalRGBA[i];
                float target = targetRGBA[i];
                rgba[i] =
                    normal + (target - normal) * frameNumber / frameCount;
            }

            component.setBackground(
                new Color(rgba[0], rgba[1], rgba[2], rgba[3]));
        }

        void start() {
            frameNumber = 0;
            timer.restart();
            component.setBackground(normalBackground);
        }

        void stop() {
            timer.stop();
            component.setBackground(normalBackground);
        }
    }

    static void addFadeOnHover(Component component,
                               Color targetBackground,
                               Color normalBackground) {

        Fader entryFader =
            new Fader(component, targetBackground, normalBackground);
        Fader exitFader =
            new Fader(component, normalBackground, targetBackground);

        component.addMouseListener(new MouseAdapter() {
            private static final long serialVersionUID = 1;

            @Override
            public void mouseEntered(MouseEvent event) {
                exitFader.stop();
                entryFader.start();
            }

            @Override
            public void mouseExited(MouseEvent event) {
                entryFader.stop();
                exitFader.start();
            }
        });
    }

    static void show() {
        Color foreground = Color.WHITE;
        Color background = new Color(0, 0, 128);
        Color highlight = Color.BLUE;

        JPopupMenu menu = new JPopupMenu();
        menu.setBackground(background);

        menu.add(new JMenuItem(String.format("%c Backup", 0x1f5ce)));
        menu.add(new JMenuItem(String.format("%c Screenshots", 0x1f5b5)));
        menu.add(new JMenuItem("\u26ed Settings"));

        // Most look-and-feels will override any colors we set, especially
        // when a menu item is selected.
        BasicMenuItemUI ui = new BasicMenuItemUI() {
            {
                selectionForeground = foreground;
                selectionBackground = background;
            }

            @Override
            public void paintBackground(Graphics g,
                                        JMenuItem item,
                                        Color background) {
                super.paintBackground(g, item, item.getBackground());
            }
        };

        for (Component item : menu.getComponents()) {
            AbstractButton b = (AbstractButton) item;
            b.setContentAreaFilled(false);
            b.setOpaque(true);
            b.setUI(ui);

            item.setForeground(foreground);
            item.setBackground(background);
            addFadeOnHover(item, highlight, background);
        }

        JLabel label = new JLabel("Right click here");
        label.setBorder(BorderFactory.createEmptyBorder(200, 200, 200, 200));
        label.setComponentPopupMenu(menu);

        JFrame frame = new JFrame("Fading Menu Test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(label);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> show());
    }
}

我编辑了它。对不起,英语和写作不好
public class ColorAnimatable extends AbstractAnimatable<Color> {

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

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

}
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
}
import java.util.Objects;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.Timer;
import javax.swing.plaf.basic.BasicMenuItemUI;

public class FadingMenuTest {
    private static final int animDurationMillis = 500;
    private static final int frameCount = 8;

    private static class Fader {
        private final Component component;

        private final Color normalBackground;
        private final float[] normalRGBA;
        private final float[] targetRGBA;
        private final float[] rgba = new float[4];

        private final Timer timer = new Timer(animDurationMillis / frameCount,
            e -> updateBackground());

        private int frameNumber;

        Fader(Component component,
              Color targetBackground,
              Color normalBackground) {

            this.component = Objects.requireNonNull(component);
            this.normalBackground = normalBackground;
            this.normalRGBA = normalBackground.getComponents(null);
            this.targetRGBA = targetBackground.getComponents(null);
        }

        private void updateBackground() {
            if (++frameNumber > frameCount) {
                timer.stop();
                return;
            }

            for (int i = rgba.length - 1; i >= 0; i--) {
                float normal = normalRGBA[i];
                float target = targetRGBA[i];
                rgba[i] =
                    normal + (target - normal) * frameNumber / frameCount;
            }

            component.setBackground(
                new Color(rgba[0], rgba[1], rgba[2], rgba[3]));
        }

        void start() {
            frameNumber = 0;
            timer.restart();
            component.setBackground(normalBackground);
        }

        void stop() {
            timer.stop();
            component.setBackground(normalBackground);
        }
    }

    static void addFadeOnHover(Component component,
                               Color targetBackground,
                               Color normalBackground) {

        Fader entryFader =
            new Fader(component, targetBackground, normalBackground);
        Fader exitFader =
            new Fader(component, normalBackground, targetBackground);

        component.addMouseListener(new MouseAdapter() {
            private static final long serialVersionUID = 1;

            @Override
            public void mouseEntered(MouseEvent event) {
                exitFader.stop();
                entryFader.start();
            }

            @Override
            public void mouseExited(MouseEvent event) {
                entryFader.stop();
                exitFader.start();
            }
        });
    }

    static void show() {
        Color foreground = Color.WHITE;
        Color background = new Color(0, 0, 128);
        Color highlight = Color.BLUE;

        JPopupMenu menu = new JPopupMenu();
        menu.setBackground(background);

        menu.add(new JMenuItem(String.format("%c Backup", 0x1f5ce)));
        menu.add(new JMenuItem(String.format("%c Screenshots", 0x1f5b5)));
        menu.add(new JMenuItem("\u26ed Settings"));

        // Most look-and-feels will override any colors we set, especially
        // when a menu item is selected.
        BasicMenuItemUI ui = new BasicMenuItemUI() {
            {
                selectionForeground = foreground;
                selectionBackground = background;
            }

            @Override
            public void paintBackground(Graphics g,
                                        JMenuItem item,
                                        Color background) {
                super.paintBackground(g, item, item.getBackground());
            }
        };

        for (Component item : menu.getComponents()) {
            AbstractButton b = (AbstractButton) item;
            b.setContentAreaFilled(false);
            b.setOpaque(true);
            b.setUI(ui);

            item.setForeground(foreground);
            item.setBackground(background);
            addFadeOnHover(item, highlight, background);
        }

        JLabel label = new JLabel("Right click here");
        label.setBorder(BorderFactory.createEmptyBorder(200, 200, 200, 200));
        label.setComponentPopupMenu(menu);

        JFrame frame = new JFrame("Fading Menu Test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(label);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> show());
    }
}