1

Guys I want to know if there is a way to make this arrow to be dragable just with X axis. I am using a null layout here, and this arrow is a jlabel that has been add into the jframe. Here is the image for more info. Thank you in advance.

(I've edited this to show some of my codes before this questions was solved. The codes here that has been shown are just about the ImageIcon, JLabel, and some part of the JFrame)

public class Level03 extends JFrame implements ActionListener, MouseListener, WindowListener {
// These are the Global Variables of the Level03 Class
ImageIcon slide = new ImageIcon("slide to unlock.png");
    ImageIcon slideButton = new ImageIcon("arrow icon.png");
    JLabel slideLabel = new JLabel(slide);
    JLabel slideArrow = new JLabel(slideButton);
Level03 (){ // This is the constructor
        // This is just setting the bounds of the arrow on the JFrame
        slideLabel.setBounds(100,350, 400,50);
        slideArrow.setBounds(117,350,50,50); // slideArrow is the JLabel with the ImageIcon that looks like an arrow.
        slideArrow.addMouseListener(this);
        mainFrame.add(slideArrow);
        mainFrame.add(slideLabel);
}

NOTE!!! I also have these overrides below from the implements ActionListener, MouseListener, WindowListener

@Override
    public void actionPerformed(ActionEvent e){}

@Override
    public void mouseClicked(MouseEvent e) {}

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}

enter image description here

  • I imagine that you would use absolute layout as you can place your components using x-y coordinates. https://www.caveofprogramming.com/guest-posts/absolute-layout-in-swing.html – Cheng Thao Dec 18 '21 at 17:59
  • Check out: https://stackoverflow.com/questions/47870238/swing-library-for-dragging-components-in-java/47871969#47871969 for basic logic for dragging a component. You would modify the code to not reset the y value. – camickr Dec 18 '21 at 18:43
  • Essentially, this is just a "slider" with some additional features (seriously that's how it was originally implemented ;)). Now, I don't think that `JSlilder` is really up to the task, not unless you're really eager to get down into the lower levels of the UI framework. Personally, I'd not be using other components for this, I'd be using some custom painting and a mouse listener to gain better control over it, but that's me – MadProgrammer Dec 18 '21 at 20:08

1 Answers1

0

Doing this kind of thing with a JLabel isn't impossible, it's just, complicated.

Personally, I'd be tempted to just use a custom painted route, as it gives you all the control.

For example...

enter image description here

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
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 {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() throws IOException {
            setBorder(new EmptyBorder(32, 32, 32, 32));
            setLayout(new GridBagLayout());
            add(new SlideToUnlock());
        }

    }

    public class SlideToUnlock extends JPanel {

        private String text;
        private Image indicatorImage;
        private Rectangle indicatorBounds;

        private Integer clickXOffset; // I can make it null and then ignore it
        private Integer dragX; // The x position of the drag

        public SlideToUnlock() throws IOException {
            indicatorImage = ImageIO.read(getClass().getResource("/images/ArrowRight.png"));
            setText("Slide to unlock");

            MouseAdapter mouseAdapter = new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    resetTimer();

                    Rectangle bounds = getIndiciatorImageBounds();
                    if (bounds.contains(e.getPoint())) {
                        clickXOffset = e.getPoint().x - bounds.x;
                    } else {
                        clickXOffset = null;
                    }
                    dragX = null;
                    repaint();
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    invalidate();
                    repaint();
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    dragX = e.getPoint().x;
                    if (didReachTheOtherSide()) {
                        // Notifiy some kind of observer
                    }
                    repaint();
                }
            };

            addMouseListener(mouseAdapter);
            addMouseMotionListener(mouseAdapter);
        }

        @Override
        public Dimension getPreferredSize() {
            FontMetrics fm = getFontMetrics(getFont());
            Image indicatorImage = getIndicatorImage();
            Insets insets = getInsets();

            int imageWidth = 0;
            int imageHeight = 0;

            if (indicatorImage != null) {
                imageWidth = indicatorImage.getWidth(this);
                imageHeight = indicatorImage.getHeight(this);
            }

            int height = Math.max(fm.getHeight(), imageHeight)
                    + 1 // Border
                    + 8; // Inner track

            int width = 1 + 8 + fm.stringWidth(getText()) + imageWidth;

            width += insets.left + insets.right;
            height += insets.top + insets.top;

            return new Dimension(width, height);
        }

        @Override
        public void invalidate() {
            super.invalidate();
            indicatorBounds = null;
            clickXOffset = null;
            dragX = null;
        }

        @Override
        public void revalidate() {
            super.revalidate();
            indicatorBounds = null;
            clickXOffset = null;
            dragX = null;
        }

        protected boolean didReachTheOtherSide() {
            Rectangle bounds = getIndiciatorImageBounds();

            return bounds.x + bounds.width >= getWidth() - 1;
        }

        protected Rectangle getIndiciatorImageBounds() {
            if (getParent() == null) {
                return null;
            }

            if (dragX == null && indicatorBounds != null) {
                return indicatorBounds;
            }

            Image indicatorImage = getIndicatorImage();
            int indicatorX = 1;
            int indicatorY = (getHeight() - indicatorImage.getHeight(this)) / 2;
            indicatorBounds = new Rectangle(indicatorX, indicatorY, indicatorImage.getWidth(this), indicatorImage.getHeight(this));

            if (dragX != null) {
                indicatorBounds.x = (indicatorBounds.x - clickXOffset) + dragX;

                if (indicatorBounds.x + indicatorBounds.width > (getWidth() - 1)) {
                    indicatorBounds.x = getWidth() - indicatorBounds.width - 1;
                } else if (indicatorBounds.x < 1) {
                    indicatorBounds.x = 1;
                }
            }

            return indicatorBounds;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public Image getIndicatorImage() {
            return indicatorImage;
        }

        public void setIndicatorImage(Image indicatorImage) {
            this.indicatorImage = indicatorImage;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            int cornerRadius = 16;

            paintText(g2d);
            paintOverlay(g2d);
            paintIndicator(g2d);

            g2d.setColor(getForeground());
            g2d.draw(new RoundRectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1, cornerRadius, cornerRadius));

            g2d.dispose();
        }

        protected void paintOverlay(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(getBackground());
            Rectangle bounds = getIndiciatorImageBounds();
            g2d.fillRect(1, 1, bounds.x + bounds.width, getHeight() - 2);
            g2d.dispose();
        }

        protected void paintText(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(getForeground());
            FontMetrics fm = g2d.getFontMetrics();
            String text = getText();
            int xPos = getWidth() - 1 - 4 - fm.stringWidth(text);
            int yPos = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
            g2d.drawString(text, xPos, yPos);
            g2d.dispose();
        }

        protected void paintIndicator(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            Rectangle bounds = getIndiciatorImageBounds();

            g2d.translate(bounds.x, bounds.y);
            Image indicatorImage = getIndicatorImage();
            g2d.drawImage(indicatorImage, 0, 0, this);
            g2d.dispose();
        }

    }
}

Ok, so, that "works", it does the job, but it's missing something ... rebound! When the user lets go of the slide control, it should animate the rebound!

(I bet you're sorry you asked now)

enter image description here

Ah, that's better, it's the small details which make it

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
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 {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() throws IOException {
            setBorder(new EmptyBorder(32, 32, 32, 32));
            setLayout(new GridBagLayout());
            add(new SlideToUnlock());
        }

    }

    public class SlideToUnlock extends JPanel {

        private String text;
        private Image indicatorImage;
        private Rectangle indicatorBounds;

        private Integer clickXOffset; // I can make it null and then ignore it
        private Integer dragX; // The x position of the drag

        private Timer reboundTimer;

        public SlideToUnlock() throws IOException {
            indicatorImage = ImageIO.read(getClass().getResource("/images/ArrowRight.png"));
            setText("Slide to unlock");

            MouseAdapter mouseAdapter = new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    resetReboundTimer();

                    Rectangle bounds = getIndiciatorImageBounds();
                    if (bounds.contains(e.getPoint())) {
                        clickXOffset = e.getPoint().x - bounds.x;
                    } else {
                        clickXOffset = null;
                    }
                    dragX = null;
                    repaint();
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    startReboundTimer();
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    dragX = e.getPoint().x;
                    if (didReachTheOtherSide()) {
                        // Notifiy some kind of observer
                    }
                    repaint();
                }
            };

            addMouseListener(mouseAdapter);
            addMouseMotionListener(mouseAdapter);
        }

        protected void resetReboundTimer() {
            if (reboundTimer == null) {
                return;
            }
            reboundTimer.stop();
            reboundTimer = null;
        }

        protected void startReboundTimer() {
            resetReboundTimer();
            Rectangle bounds = getIndiciatorImageBounds();

            clickXOffset = 0;
            dragX = bounds.x + (bounds.width / 2);

            int lowerRange = 1 + (bounds.width / 2);
            int upperRange = getWidth() - 1 - (bounds.width / 2);

            int fullRange = upperRange - lowerRange;
            int currentRange = (bounds.x + (bounds.width / 2)) - lowerRange;
            double progressRange = currentRange / (double) fullRange;
            Duration fullDuration = Duration.ofMillis(250);
            Duration desiredDuration = Duration.ofMillis((long) (fullDuration.toMillis() * progressRange));

            int remainingRange = (int) (fullRange * progressRange);

            reboundTimer = new Timer(5, new ActionListener() {
                private Instant startTime = null;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (startTime == null) {
                        startTime = Instant.now();
                    }
                    Duration runTime = Duration.between(startTime, Instant.now());
                    double runTimeProgress = runTime.toMillis() / (double) desiredDuration.toMillis();
                    if (runTimeProgress >= 1.0) {
                        resetReboundTimer();
                        invalidate();
                    } else {
                        dragX = (int) (remainingRange * (1.0 - runTimeProgress));
                    }

                    repaint();
                }
            });
            reboundTimer.setInitialDelay(0);
            reboundTimer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            FontMetrics fm = getFontMetrics(getFont());
            Image indicatorImage = getIndicatorImage();
            Insets insets = getInsets();

            int imageWidth = 0;
            int imageHeight = 0;

            if (indicatorImage != null) {
                imageWidth = indicatorImage.getWidth(this);
                imageHeight = indicatorImage.getHeight(this);
            }

            int height = Math.max(fm.getHeight(), imageHeight)
                    + 1 // Border
                    + 8; // Inner track

            int width = 1 + 8 + fm.stringWidth(getText()) + imageWidth;

            width += insets.left + insets.right;
            height += insets.top + insets.top;

            return new Dimension(width, height);
        }

        @Override
        public void invalidate() {
            super.invalidate();
            indicatorBounds = null;
            clickXOffset = null;
            dragX = null;
        }

        @Override
        public void revalidate() {
            super.revalidate();
            indicatorBounds = null;
            clickXOffset = null;
            dragX = null;
        }

        protected boolean didReachTheOtherSide() {
            Rectangle bounds = getIndiciatorImageBounds();

            return bounds.x + bounds.width >= getWidth() - 1;
        }

        protected Rectangle getIndiciatorImageBounds() {
            if (getParent() == null) {
                return null;
            }

            if (dragX == null && indicatorBounds != null) {
                return indicatorBounds;
            }

            Image indicatorImage = getIndicatorImage();
            int indicatorX = 1;
            int indicatorY = (getHeight() - indicatorImage.getHeight(this)) / 2;
            indicatorBounds = new Rectangle(indicatorX, indicatorY, indicatorImage.getWidth(this), indicatorImage.getHeight(this));

            if (dragX != null) {
                indicatorBounds.x = (indicatorBounds.x - clickXOffset) + dragX;

                if (indicatorBounds.x + indicatorBounds.width > (getWidth() - 1)) {
                    indicatorBounds.x = getWidth() - indicatorBounds.width - 1;
                } else if (indicatorBounds.x < 1) {
                    indicatorBounds.x = 1;
                }
            }

            return indicatorBounds;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public Image getIndicatorImage() {
            return indicatorImage;
        }

        public void setIndicatorImage(Image indicatorImage) {
            this.indicatorImage = indicatorImage;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            int cornerRadius = 16;

            paintText(g2d);
            paintOverlay(g2d);
            paintIndicator(g2d);

            g2d.setColor(getForeground());
            g2d.draw(new RoundRectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1, cornerRadius, cornerRadius));

            g2d.dispose();
        }

        protected void paintOverlay(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(getBackground());
            Rectangle bounds = getIndiciatorImageBounds();
            g2d.fillRect(1, 1, bounds.x + bounds.width, getHeight() - 2);
            g2d.dispose();
        }

        protected void paintText(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(getForeground());
            FontMetrics fm = g2d.getFontMetrics();
            String text = getText();
            int xPos = getWidth() - 1 - 4 - fm.stringWidth(text);
            int yPos = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
            g2d.drawString(text, xPos, yPos);
            g2d.dispose();
        }

        protected void paintIndicator(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            Rectangle bounds = getIndiciatorImageBounds();

            g2d.translate(bounds.x, bounds.y);
            Image indicatorImage = getIndicatorImage();
            g2d.drawImage(indicatorImage, 0, 0, this);
            g2d.dispose();
        }

    }
}

Now, doing this with labels would follow a very similar course, the only addition would be, you'd have to manage the label size and position yourself (as most layout managers won't allow you to this)

The following are some examples of dragging a label via a MouseMotionListener

Now, in your case, you don't really care about the y position, so that can remain centred relative to the container (or the track) and you'd just need to update the x position based on the click offset and the current drag position. Complications arise in the fact that you'd need to be monitoring the label for drags, but converting the position of the movement to the parent container. Doable, but it's an additional complication

With a JLabel

The basic workflow is the same, except now you need to manage the component state of the label (in fact labels, because I assume you'll also want to have some text).

This is a basic example which allows you to drag the a label, it's core workflow is basically the same as the previous examples, but it has the added overhead of needing to manage the component(s) size and positions as well

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() throws IOException {
            setBorder(new EmptyBorder(32, 32, 32, 32));
            setLayout(new GridBagLayout());
            add(new SlideToUnlock());
        }

    }

    public class SlideToUnlock extends JPanel {

        private JLabel indicatorLabel;
        private JPanel overLayPane;
        private JLabel textLabel;

        private Integer clickXOffset;
        private Integer dragX;

        public SlideToUnlock() throws IOException {
            setLayout(null);
            setBorder(new LineBorder(getForeground(), 1, true));
            indicatorLabel = new JLabel(new ImageIcon(ImageIO.read(getClass().getResource("/images/ArrowRight.png"))));
            indicatorLabel.setBounds(1, 1, indicatorLabel.getPreferredSize().width, indicatorLabel.getPreferredSize().height);
            add(indicatorLabel);

            overLayPane = new JPanel();
            add(overLayPane);

            textLabel = new JLabel("Slide to unlock");
            textLabel.setBounds(1, 1, textLabel.getPreferredSize().width, textLabel.getPreferredSize().height);
            add(textLabel);

            MouseAdapter mouseAdapter = new MouseAdapter() {

                protected void resetDrag() {
                    clickXOffset = null;
                    dragX = null;
                }

                @Override
                public void mousePressed(MouseEvent e) {
                    Point point = e.getPoint();
                    Point localPoint = SwingUtilities.convertPoint(SlideToUnlock.this, point, indicatorLabel);
                    if (indicatorLabel.getBounds().contains(localPoint)) {
                        clickXOffset = point.x - indicatorLabel.getBounds().x;
                    } else {
                        resetDrag();
                    }
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    resetDrag();
                    doLayout();
                    repaint();
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    dragX = e.getPoint().x;
                    doLayout();
                    repaint();
                }
            };

            addMouseListener(mouseAdapter);
            addMouseMotionListener(mouseAdapter);
        }

        @Override
        public void doLayout() {
            Dimension preferredSize = indicatorLabel.getPreferredSize();
            int xPos = 1;
            if (dragX != null) {
                xPos = (1 - clickXOffset) + dragX;

                if (xPos + preferredSize.width > (getWidth() - 1)) {
                    xPos = getWidth() - preferredSize.width - 1;
                } else if (xPos < 1) {
                    xPos = 1;
                }
            }
            indicatorLabel.setBounds(xPos, 1, indicatorLabel.getPreferredSize().width, getHeight() - 2);
            overLayPane.setBounds(1, 1, indicatorLabel.getX() + indicatorLabel.getWidth() - 1, getHeight() - 2);
            textLabel.setBounds(getWidth() - textLabel.getPreferredSize().width - 4, 1, textLabel.getPreferredSize().width, getHeight() - 2);
        }

        @Override
        public Dimension getPreferredSize() {

            Dimension preferredSize = indicatorLabel.getPreferredSize();
            preferredSize.width = preferredSize.width * 4;

            int height = Math.max(indicatorLabel.getPreferredSize().height, textLabel.getPreferredSize().height);
            int width = indicatorLabel.getPreferredSize().width + 4 + textLabel.getPreferredSize().width + 4 + 1;

            Insets insets = getInsets();

            width += insets.left + insets.right;
            height += insets.top + insets.bottom;

            return new Dimension(width, height);
        }

    }
}

This a quick "hack". You'll need to provide addition functionality to change the text, support changes to the background color and update the state when it does change

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thank you so much for the answer, but I'm just new to java and I can't read all of these codes properly. I really do appreciate the answer. But can someone tell me where is the part here (With `JLabel`) that makes the button moves horizontally and became clickable? I want that one with the rebound, it looks so good! I know this is too much to ask. –  Dec 19 '21 at 03:56
  • I am using a `null JFrame` and that "slide left to unlock" with border was just an image that I've manually add using `.setBounds();`. Is there and easier way to explain to me where I can understand the part where the `JLabel` with and `ImageIcon`, has became a movable `JLabel`? Thank you so much. –  Dec 19 '21 at 03:56
  • I've edited my post and add some of the codes that I currently have. I wish you could help me, I know that this is not a "Code Factory" but I'm too pressed for time for my project and some of I put on my codes are still not taught to us. Thank you. –  Dec 19 '21 at 04:25
  • 1
    The label is controlled through the use of the MouseAdapter. The overall workflow between the two examples is basically the same, the core difference is in how the content is rendered – MadProgrammer Dec 19 '21 at 06:54
  • I think I will let go about this and change the content of my level. I've been stuck on this for too long and now I only got 12 hours till our deadline and I still have 7 levels to do. Thank you so much for the answer, I wish someone who understand this will found this useful. Thank you again @MadProgrammer , I hope you help many more people. –  Dec 19 '21 at 07:04
  • [How to Write a Mouse Listener](https://docs.oracle.com/javase/tutorial/uiswing/events/mouselistener.html) might be useful – MadProgrammer Dec 19 '21 at 07:53