JButton在Java Swing中悬停动画

use*_*287 2 java user-interface swing

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

像这样的东西:

在此输入图像描述

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

在此输入图像描述

随着翻滚它需要GIF和不同的屏幕尺寸将需要不同的GIF尺寸.

有没有办法做一些如何抛出代码?

Mad*_*mer 8

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

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

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

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

我的第一个建议是使用一个好的动画框架,比如TimingFramework,它可以为你完成所有繁重的工作.

如果你不能做到这一点,那么你就是一个有趣的旅程.

第一个,实际上很简单,就是找到一个好的颜色混合算法.我花了很多时间探索不同的算法并尝试了一堆不同的东西,然后再解决类似......

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;
}
Run Code Online (Sandbox Code Playgroud)

我使用这种方法的原因是我可以轻松地将颜色移向黑色或白色,这是许多其他方法无法实现的(只是将颜色移动到高或低范围).这也着重于基于对之间的比率混合两种颜色一起01(因此在0.5有平衡两者共混物).

动画引擎......

注意:Swing是单线程的而不是线程安全的,在开发解决方案时需要考虑到这一点.好的,困难的部分......

从一点开始直到你到达另一点的结尾,动画很容易.这个问题是,它们不能很好地扩展,并且通常不会产生良好的自然感觉动画.

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

这种方法可以很好地扩展,你可以改变时间而不关心其他任何东西,它会照顾好自己.它也适用于性能可能达不到杀手帧速率的系统,因为它们可以丢帧并且大多数"伪造"它

要记住一个关键概念,动画是随时间变化的"错觉".

Range

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

}
Run Code Online (Sandbox Code Playgroud)

愚蠢的是,我做了很多,所以,这里有一个帮助类来封装核心概念.

Animator

这是一个驱动所有动画的"中央"引擎.它大约每5毫秒重复一次,并驱动Animatable对象,实际上通过动画计算时间.

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

}
Run Code Online (Sandbox Code Playgroud)

Animatable

这是可以动画的"东西".它有一个概念a Range和a Duration应该是动画的(它也支持缓和,但我稍后会讨论)

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();
}
Run Code Online (Sandbox Code Playgroud)

AbstractAnimatable

一个抽象的实现Animatable.这会对每个计算所做的所有很酷,很棒的计算,tick以确定它在给定的动画周期中有多远,并向已注册的听众生成关于状态变化的通知,因此他们可以实际做一些事情......

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

}
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

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

    }

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

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...

Menu

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
}
Run Code Online (Sandbox Code Playgroud)

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

  • 你今晚认真地写下了这一切吗?感动! (2认同)
  • 其中大部分来自我之前使用的库或代码片段 - 我已经完成了很多基于动画的解决方案 - 这是一种激情:P (2认同)