I\'m trying to move a JButton to the location of another one but the button I want to move moves to a wrong point. My idea is that this happens because I use multiple JPanels. I
Maybe use mousePressed()
,when you move the card,you press it until the target.And in the process, you get the information about JButton.getLocation()
by the event,and than you need to solve the collision problem between two cards.So it's good!Of course, this is my advice ,you should have better idea!
Animating the button movement isn't actually the hardest problem, the hardest problem is trying to move the data about in away in which you can manage it and how to connect the source component with the target...
To start with, you need a means by which you can move a component across container boundaries. While there are probably a few ways to do this, the simplest is to probably use the glass pane of the frame
public class AnimationPane extends JPanel {
public AnimationPane() {
setOpaque(false);
setLayout(null);
}
}
This is nothing special, it's just a JPanel
which is transparent and has no layout manager, normally, not recommended, but in the case, we're going to take control..
Now, we need some way to animate the movement...
public enum Animator {
INSTANCE;
private List<IAnimatable> animatables;
private Timer timer;
private Animator() {
animatables = new ArrayList<>(25);
timer = new Timer(40, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
IAnimatable[] anins = animatables.toArray(new IAnimatable[animatables.size()]);
for (IAnimatable animatable : anins) {
animatable.update();
}
}
});
timer.start();
}
public void addAnimatable(IAnimatable animatable) {
animatables.add(animatable);
}
public void removeAnimatable(IAnimatable animatable) {
animatables.remove(animatable);
}
}
public interface IAnimatable {
public void update();
}
public interface IMoveAnimatable extends IAnimatable{
public JComponent getSourceComponent();
public IImportable getImportable();
}
So the Animator
is the core "engine", it's basically a Swing Timer
which simply calls update
on any IAnimatable
s it might be managing. The intention with this approach is you can have a number of animations running, but it won't degrade the system (greatly) as you only have a single update/timer point.
Now, normally I'd just use something like the Timing Framework or the Trident Framework or even the Universal Tween Engine
The IAnimatable
interfaces just define the basic contracts that provide functionality for the animation.
We need to define some kind of contract the defines objects which can take part in the animation process and receive information, or the "target"
public interface IImportable {
public JComponent getView();
public void importValue(String value);
}
public abstract class AbstractImportable extends JPanel implements IImportable {
@Override
public JComponent getView() {
return this;
}
}
Now it occurs to me that we could tap into the pre-existing Transferable
API, which would allow you to also implement drag-n-drop (and even copy/cut and paste), this would be used to define a lookup mechanism where you match a given data type with potential targets based on the DataFlavor
... but I'll leave you to investigate how that might work...
The core mechanism basically removes the source component from it's current container, adds it to the AnimationPane
, moves the source component across the AnimationPane
and then imports the data into the target...
The problem is, you need to translate the location of component from it's current context to the AnimationPane
.
A components location is relative to it's parents context. It's relatively easy to do with SwingUtilities.convertPoint(Component, Point, Component)
We calculate the origin point of the source component and the target point, relative to the AnimationPane
. We then, on each call to update
, calculate the progress of the animation. Instead of using a "delta" movement, we calculate the different between the time we started and a predefined duration (1 second in this case), this generally produces a more flexible animation
public class DefaultAnimatable implements IMoveAnimatable {
public static final double PLAY_TIME = 1000d;
private Long startTime;
private JComponent sourceComponent;
private IImportable importable;
private JComponent animationSurface;
private Point originPoint, destinationPoint;
private String value;
public DefaultAnimatable(JComponent animationSurface, JComponent sourceComponent, IImportable importable, String value) {
this.sourceComponent = sourceComponent;
this.importable = importable;
this.animationSurface = animationSurface;
this.value = value;
}
public String getValue() {
return value;
}
public JComponent getAnimationSurface() {
return animationSurface;
}
@Override
public JComponent getSourceComponent() {
return sourceComponent;
}
@Override
public IImportable getImportable() {
return importable;
}
@Override
public void update() {
if (startTime == null) {
System.out.println("Start");
IImportable importable = getImportable();
JComponent target = importable.getView();
originPoint = SwingUtilities.convertPoint(getSourceComponent().getParent(), getSourceComponent().getLocation(), getAnimationSurface());
destinationPoint = SwingUtilities.convertPoint(target.getParent(), target.getLocation(), getAnimationSurface());
destinationPoint.x = destinationPoint.x + ((target.getWidth() - getSourceComponent().getWidth()) / 2);
destinationPoint.y = destinationPoint.y + ((target.getHeight() - getSourceComponent().getHeight()) / 2);
Container parent = getSourceComponent().getParent();
getAnimationSurface().add(getSourceComponent());
getSourceComponent().setLocation(originPoint);
parent.invalidate();
parent.validate();
parent.repaint();
startTime = System.currentTimeMillis();
}
long duration = System.currentTimeMillis() - startTime;
double progress = Math.min(duration / PLAY_TIME, 1d);
Point location = new Point();
location.x = progress(originPoint.x, destinationPoint.x, progress);
location.y = progress(originPoint.y, destinationPoint.y, progress);
getSourceComponent().setLocation(location);
getAnimationSurface().repaint();
if (progress == 1d) {
getAnimationSurface().remove(getSourceComponent());
Animator.INSTANCE.removeAnimatable(this);
animationCompleted();
}
}
public int progress(int startValue, int endValue, double fraction) {
int value = 0;
int distance = endValue - startValue;
value = (int) Math.round((double) distance * fraction);
value += startValue;
return value;
}
protected void animationCompleted() {
getImportable().importValue(getValue());
}
}
Okay, now this produces a linear animation, which is pretty boring, now if you have plenty of time, you could create an easement like this or just use one of the animation frameworks...
Now, we need to put it together...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.LineBorder;
public class AnimationTest {
public static void main(String[] args) {
new AnimationTest();
}
public AnimationTest() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
AnimationPane animationPane = new AnimationPane();
LeftPane leftPane = new LeftPane(animationPane);
RightPane rightPane = new RightPane();
leftPane.setImportabale(rightPane);
rightPane.setImportabale(leftPane);
JFrame frame = new JFrame("Testing");
frame.setLayout(new GridLayout(1, 2));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(leftPane, BorderLayout.WEST);
frame.add(rightPane, BorderLayout.WEST);
frame.setGlassPane(animationPane);
animationPane.setVisible(true);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class RightPane extends AbstractImportable {
private IImportable source;
private JButton imported;
private String importedValue;
public RightPane() {
setLayout(new GridBagLayout());
setBorder(new LineBorder(Color.DARK_GRAY));
}
public void setImportabale(IImportable source) {
this.source = source;
}
@Override
public void importValue(String value) {
if (imported != null) {
// May re-animate the movement back...
remove(imported);
}
importedValue = value;
imported = new JButton(">> " + value + "<<");
add(imported);
revalidate();
repaint();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
public class LeftPane extends AbstractImportable {
private IImportable importable;
public LeftPane(AnimationPane animationPane) {
setLayout(new GridBagLayout());
JButton btn = new JButton("Lefty");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DefaultAnimatable animatable = new DefaultAnimatable(animationPane, btn, importable, "Lefty");
Animator.INSTANCE.addAnimatable(animatable);
}
});
add(btn);
setBorder(new LineBorder(Color.DARK_GRAY));
}
public void setImportabale(IImportable target) {
this.importable = target;
}
@Override
public void importValue(String value) {
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
}