我使用Swing在GUI窗口上工作,我想创建一个导航菜单。我想用鼠标悬停在导航菜单的按钮上时添加动画。
像这样的东西:
我无法使用鼠标侦听器执行此操作,因为它只是在颜色之间变化而没有动画,就像这样
使用rolloving时需要gif,并且不同的屏幕尺寸将需要不同的gif尺寸。
有办法做到一些如何抛出代码吗?
最佳答案
首先,您实际上是在问一个非常困难和复杂的问题。正确完成动画并不容易,需要大量状态管理。
例如,如果用户在动画完成前将鼠标从菜单项中移出,会发生什么情况?您是否从当前状态进行动画制作?您是从最终状态开始制作动画吗?您是在整个时间范围内还是仅在剩余时间范围内进行动画处理?您将如何控制和管理所有这一切!?
您还询问有关跨颜色范围设置动画的问题。色彩融合实际上并不像听起来那样容易。当然,您可能会说您只是在设置Alpha值的动画,但是混合颜色为您提供了更大的机会来提供不同的动画效果,例如从透明的绿色到不透明的蓝色混合,很酷。
动画通常也不是线性的,有很多理论可以很好地制作动画,包括预期,挤压恶臭,登台,等等,等等,老实说,还有比我更好的人帮助您解决这一问题。关键是,好的动画很复杂。
我的第一个建议是使用一个好的动画框架,例如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;
}
我使用这种方法的原因是,我可以轻松地将颜色移向黑色或白色,而其他许多方法都无法实现(只需将颜色移至高或低范围)。这也着重于根据
0
和1
之间的比率将两种颜色混合在一起(因此,在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);
}
愚蠢的事情是,我经常这样做,因此,这里有一个帮助程序类来封装核心概念。
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();
}
}
}
Animatable
这是可以动画的“东西”。它具有
Range
和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();
}
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;
}
}
AnimatableListener
侦听器/观察者更改为
AbstractAnimatable
状态。这样,AbstractAnimatable
可以告诉相关方该状态已更新以及该状态当前是什么。public interface AnimatableListener<T> {
public void stateChanged(Animatable<T> animator);
}
Easement
好的,所以我提到了“动画理论”。基本上,这是“样条插值”的一种实现,旨在提供常见的动画概念(慢进,慢出等)。这是通过动画更改“进度”值,以便动画的“速度”“出现”,以便在动画的持续时间内进行更改。
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;
}
}
}
所以呢?!
好吧,大约现在,您可能正在挠头,希望您没有问这个问题;)
所有这些如何帮助。关键是,以上所有内容都是可重复使用的,因此您可以将其转储到某个地方的库中,而不在乎:/
然后,您要做的就是实现所需的功能并应用...
ColorRange
这采用了以前的
colorBlend
算法,并包装成Range
概念...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;
}
}
这使我们可以根据动画的进度来计算所需的颜色
ColorAnimatable
然后创建“可动画的颜色”概念。
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());
}
}
这看起来很复杂,但是并不难。基本上,它定义了焦点和未焦点的颜色,设置了
MouseListener
来监视mouseEntered
和mouseExited
事件,并根据所需的过渡设置了ColorAnimatable
。这可能做的一件事(可能并不明显)是,它会考虑未完成的动画。因此,如果用户在焦点动画完成之前将其移出项目,它将使用剩余时间和当前颜色作为非焦点过渡的起点。
由于Swing组件是完全透明或完全不透明的,因此我们需要“伪造” alpha支持。为此,我们使组件完全透明,然后自己绘制背景
可运行的示例...
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
}
您也可以看看:
The Universal Tween Engine(尚未使用)
The TimingFramework(上面的很多内容都是基于宽松的)
The Trident Animation Library框架中的Radiance(未使用)
您还应该查看Concurrency in Swing,以更好地理解引擎本身如何工作以及为什么这样做的基本概念。