Okay, this took a little longer then I wanted (10 month olds don't have any patience)
The basic concept revolves around the idea that you need to change from one state to another over a period of time.
Given a start time and the current time, we can calculate the amount of time that the animation has been running, and given the total animation time, the current progress.
With this (and some clever maths) we can calculate the current state from our start state towards our target state.
I've done movement as well, so that may be a little over kill, but the basic premise remains the same.
I place stateful information about the nodes that need to change in animation properties class and use a javax.swing.Timer
to tick over the animation (at a reasonably steady rate). I then update the state of each node as required and repaint the screen.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.management.StringValueExp;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class AnimateNode {
public static void main(String[] args) {
new AnimateNode();
}
public AnimateNode() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new NodePane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface Node {
public void paint(JComponent parent, Graphics2D g2d);
public void setColor(Color color);
public Color getColor();
public Node getParent();
public Node getLeft();
public Node getRight();
public void setLeftNode(Node node);
public void setRightNode(Node node);
public Point getLocation();
public void setLocation(Point p);
}
public class DefaultNode implements Node {
private int number;
private Node parent;
private Node left;
private Node right;
private Point location;
private Color color;
public DefaultNode(int number, Node parent) {
this.parent = parent;
color = UIManager.getColor("Panel.background");
this.number = number;
}
public void setLeftNode(Node left) {
this.left = left;
}
public void setRightNode(Node right) {
this.right = right;
}
public Node getParent() {
return parent;
}
public Node getLeft() {
return left;
}
public Node getRight() {
return right;
}
@Override
public Point getLocation() {
return location;
}
@Override
public void setLocation(Point location) {
this.location = location;
}
@Override
public void paint(JComponent parent, Graphics2D g2d) {
FontMetrics fm = g2d.getFontMetrics();
int radius = fm.getHeight();
Point p = getLocation();
int x = p.x - (radius / 2);
int y = p.y - (radius / 2);
Ellipse2D node = new Ellipse2D.Float(x, y, radius, radius);
g2d.setColor(getColor());
g2d.fill(node);
g2d.setColor(Color.GRAY);
g2d.draw(node);
String text = String.valueOf(number);
x = x + ((radius - fm.stringWidth(text)) / 2);
y = y + (((radius - fm.getHeight()) / 2) + fm.getAscent());
g2d.drawString(text, x, y);
}
@Override
public void setColor(Color color) {
this.color = color;
}
@Override
public Color getColor() {
return color;
}
@Override
public String toString() {
return number + " @ " + getLocation();
}
}
public class AnimationProperties {
private Point startPoint;
private Point targetPoint;
private Color startColor;
private Color endColor;
private Node node;
public AnimationProperties(Node node) {
this.node = node;
}
public Node getNode() {
return node;
}
public void setTargetColor(Color endColor) {
this.endColor = endColor;
}
public void setStartColor(Color startColor) {
this.startColor = startColor;
}
public void setStartPoint(Point startPoint) {
this.startPoint = startPoint;
}
public void setTargetPoint(Point targetPoint) {
this.targetPoint = targetPoint;
}
public Color getTargetColor() {
return endColor;
}
public Color getStartColor() {
return startColor;
}
public Point getStartPoint() {
return startPoint;
}
public Point getTargetPoint() {
return targetPoint;
}
public Point getLocation(float progress) {
return calculateProgress(getStartPoint(), getTargetPoint(), progress);
}
public Color getColor(float progress) {
return blend(getStartColor(), getTargetColor(), 1f - progress);
}
public void update(float progress) {
node.setLocation(getLocation(progress));
node.setColor(getColor(progress));
}
}
public class NodePane extends JPanel {
private int number;
private Node root;
private Map<Node, AnimationProperties> aniProperties;
private Timer animationTimer;
private Timer startTimer;
private long startTime;
private int runTime = 1000;
public NodePane() {
aniProperties = new HashMap<>(25);
root = addLeftNode(null);
root.setColor(getBackground());
addMouseListener(new MouseAdapter() {
private Random rand;
@Override
public void mouseClicked(MouseEvent e) {
generateNextNode(root);
revalidate();
// repaint();
}
protected void generateNextNode(Node parent) {
Node child = null;
if (rand == null) {
rand = new Random(System.currentTimeMillis());
}
boolean left = rand.nextBoolean();
if (left) {
child = parent.getLeft();
} else {
child = parent.getRight();
}
if (child == null) {
if (left) {
addLeftNode(parent);
} else {
addRightNode(parent);
}
} else {
generateNextNode(child);
}
}
});
startTimer = new Timer(250, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
stopAnimation();
startTime = -1;
animationTimer.start();
}
});
startTimer.setRepeats(false);
animationTimer = new Timer(40, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (startTime < 0) {
startTime = System.currentTimeMillis();
}
float progress = 1f;
long duration = System.currentTimeMillis() - startTime;
if (duration >= runTime) {
((Timer) e.getSource()).stop();
} else {
progress = (float) duration / (float) runTime;
}
for (AnimationProperties ap : aniProperties.values()) {
ap.update(progress);
}
repaint();
if (progress == 1f) {
aniProperties.clear();
}
}
});
animationTimer.setRepeats(true);
animationTimer.setCoalesce(true);
}
protected void stopAnimation() {
if (animationTimer.isRunning()) {
animationTimer.stop();
for (AnimationProperties ap : aniProperties.values()) {
Node node = ap.getNode();
ap.setStartColor(node.getColor());
ap.setStartPoint(node.getLocation());
}
}
}
public Point getStartPoint(Node node) {
Point startPoint = node.getLocation();
while (startPoint == null) {
node = node.getParent();
startPoint = node.getLocation();
}
return startPoint;
}
protected void layoutNode(Node node, int x, int y) {
if (node != null) {
FontMetrics fm = getFontMetrics(getFont());
int nodeHeight = fm.getHeight();
if (node.getParent() != null) {
Point p = new Point(x, y);
Point sp = getStartPoint(node);
if (node.getLocation() == null) {
System.out.println("new node " + node);
}
if (node.getLocation() == null || !p.equals(node.getLocation())) {
AnimationProperties ap = new AnimationProperties(node);
ap.setStartColor(node.getColor());
ap.setTargetColor(getBackground());
ap.setStartPoint(sp);
ap.setTargetPoint(new Point(x, y));
node.setLocation(sp);
aniProperties.put(node, ap);
System.out.println("New Node to " + node);
} else {
aniProperties.remove(node);
}
} else {
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…