1

I made a button by extending JPanel and adding 2 JLabels on top of each other using the OverlayLayout. one of the JLabels is the icon which is just a font-awesome icon. The other one is the hover text. I want the background of the JPanel to be transparent, so it doesn't have a white border. I did this using this AlphaContainer method:

https://www.programcreek.com/java-api-examples/index.php?source_dir=cismet-gui-commons-master/src/main/java/de/cismet/tools/gui/AlphaContainer.java

This is what I mean by slow hovering:

enter image description here

I now only added the setOpaque(false) property to the bottom half of the icons. This is what it looks like: enter image description here

Here I added the 6 buttons:

setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));

        HoverButtonTest forum = new HoverButtonTest("\uf086", "Forum", Settings.iconSize);
        add(forum);

        HoverButtonTest hs = new HoverButtonTest("\uf080", "Highscores", Settings.iconSize);
        add(hs);

        HoverButtonTest shop = new HoverButtonTest("\uf07a", "Store", Settings.iconSize);
        add(shop);

        HoverButtonTest vote = new HoverButtonTest("\uf046", "Vote for Us", Settings.iconSize);
        vote.setOpaque(false);
        add(vote);

        HoverButtonTest discord = new HoverButtonTest("\uf392", "Discord", Settings.iconSize, HoverButtonTest.Type.BRAND);
        discord.setOpaque(false);
        add(discord);

        HoverButtonTest web = new HoverButtonTest("\uf0ac", "Web", Settings.iconSize, HoverButtonTest.Type.SOLID);
        web.setOpaque(false);
        add(web);

This is the button class I am using:

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

/**
 * @author Ruud.
 */
public class HoverButtonTest extends JPanel implements MouseListener {

    public enum Type {
        NORMAL("Font Awesome.ttf"),
        REGULAR("Font Awesome Regular.ttf"),
        SOLID("Font Awesome Solid.ttf"),
        BRAND("Font Awesome Brands.ttf");

        String font;

        Type(String font) {
            this.font = font;
        }

        String getFont() {
            return font;
        }

    }

    private JLabel icon;
    private JLabel text;

    public HoverButtonTest(String icon, String text, int size) {
        this(icon, text, size, Type.NORMAL);
    }

    public HoverButtonTest(String icon, String text, int size, Type type) {
        setLayout(new OverlayLayout(this));
        addMouseListener(this);
        setOpaque(false);

        this.text = new JLabel(text);
        this.text.setForeground(Color.WHITE);
        this.text.setAlignmentX(0.5f);
        this.text.setAlignmentY(0.5f);
        Utils.setFont(this.text, "OpenSans-Light.ttf", 13);
        this.text.setVisible(false);
        add(this.text);

        this.icon = new JLabel(icon);
        this.icon.setForeground(Settings.primaryColor);
        this.icon.setAlignmentX(0.5f);
        this.icon.setAlignmentY(0.5f);
        Utils.setFont(this.icon, type.getFont(), size);
        add(this.icon);
    }

    @Override
    public void paintComponent(Graphics g) {
        g.setColor(new Color(0, 0, 0, 0));
    }


    @Override
    public void mouseClicked(MouseEvent e) {

    }

    @Override
    public void mousePressed(MouseEvent e) {

    }

    @Override
    public void mouseReleased(MouseEvent e) {

    }

    @Override
    public void mouseEntered(MouseEvent e) {
        text.setVisible(true);
        icon.setForeground(Settings.primaryColor.darker().darker().darker().darker());
    }

    @Override
    public void mouseExited(MouseEvent e) {
        text.setVisible(false);
        icon.setForeground(Settings.primaryColor);
    }
}

I've edited the code from RadioDef to fit a bit more to my needs, the weird thing is that it doesn't occur in this code

import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;

public class MouseoverPanels {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(MouseoverPanels::new);
    }

    public MouseoverPanels() {
        JFrame frame = new JFrame("Mouseover Panels");
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        JPanel background = new Background();

        JPanel buttons = new Buttons();
        //JPanel buttons = new JPanel();
        buttons.setOpaque(false);

        buttons.add(new MouseoverPanel("Queen", QUEEN_IMG, QUEEN_IMG_H));
        buttons.add(new MouseoverPanel("King", KING_IMG, KING_IMG_H));
        buttons.add(new MouseoverPanel("Rook", ROOK_IMG, ROOK_IMG_H));
        buttons.add(new MouseoverPanel("Knight", KNIGHT_IMG, KNIGHT_IMG_H));
        buttons.add(new MouseoverPanel("Pawn", PAWN_IMG, PAWN_IMG_H));

        background.add(buttons);

        frame.setContentPane(background);

        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    class MouseoverPanel extends JPanel {
        JLabel button;
        JLabel hoverImage;
        JLabel icon;

        MouseoverPanel(String text, Image img, Image hImg) {
            setLayout(new OverlayLayout(this));
            setOpaque(false);

            button = new JLabel(text);
            button.setOpaque(false);
            button.setAlignmentX(0.5f);
            button.setAlignmentY(0.5f);
            button.setVisible(false);

            hoverImage = new JLabel(new ImageIcon(hImg));
            hoverImage.setOpaque(false);
            hoverImage.setAlignmentX(0.5f);
            hoverImage.setAlignmentY(0.5f);
            hoverImage.setVisible(false);

            icon = new JLabel(new ImageIcon(img));
            icon.setOpaque(false);
            icon.setAlignmentX(0.5f);
            icon.setAlignmentY(0.5f);

            add(button);
            add(hoverImage);
            add(icon);

            MouseoverListener ml = new MouseoverListener();

            addMouseListener(ml);
            button.addMouseListener(ml);
            icon.addMouseListener(ml);
        }

        class MouseoverListener extends MouseAdapter {
            @Override
            public void mouseEntered(MouseEvent e) {
                button.setVisible(true);
                hoverImage.setVisible(true);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                button.setVisible(false);
                hoverImage.setVisible(false);
            }
        }
    }

    class Buttons extends JPanel {
        public Buttons() {
            setOpaque(false);
            setBorder(new LineBorder(Color.BLACK));
        }
        @Override
        protected void paintComponent(Graphics g) {
            g.setColor(new Color(0, 0, 0, 100));
            g.fillRect(0, 0, getWidth(), getHeight());
        }
    }

    class Background extends JPanel {
        private BufferedImage sprite = null;

        public Background() {
            try {
                sprite = ImageIO.read(new URL("https://i.stack.imgur.com/XZ4V5.jpg"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Image scaledImage = sprite.getScaledInstance(getWidth(), getHeight(), Image.SCALE_REPLICATE);
            g.drawImage(scaledImage, getWidth() / 2 - scaledImage.getWidth(this) / 2, getHeight() / 2 - scaledImage.getHeight(this) / 2, this);
        }
    }

    static final Image QUEEN_IMG,
            QUEEN_IMG_H,
            KING_IMG,
            KING_IMG_H,
            ROOK_IMG,
            ROOK_IMG_H,
            KNIGHT_IMG,
            KNIGHT_IMG_H,
            PAWN_IMG,
            PAWN_IMG_H;

    static {
        try {
            // source for sprite sheet: https://stackoverflow.com/a/19209651/2891664
            BufferedImage sprites = ImageIO.read(new URL("https://i.stack.imgur.com/memI0.png"));
            int n = 64;
            QUEEN_IMG = sprites.getSubimage(0 * n, 0, n, n);
            QUEEN_IMG_H = sprites.getSubimage(0 * n, 64, n, n);
            KING_IMG = sprites.getSubimage(1 * n, 0, n, n);
            KING_IMG_H = sprites.getSubimage(1 * n, 64, n, n);
            ROOK_IMG = sprites.getSubimage(2 * n, 0, n, n);
            ROOK_IMG_H = sprites.getSubimage(2 * n, 64, n, n);
            KNIGHT_IMG = sprites.getSubimage(3 * n, 0, n, n);
            KNIGHT_IMG_H = sprites.getSubimage(3 * n, 64, n, n);
            PAWN_IMG = sprites.getSubimage(4 * n, 0, n, n);
            PAWN_IMG_H = sprites.getSubimage(4 * n, 64, n, n);
        } catch (IOException x) {
            throw new UncheckedIOException(x);
        }
    }


}

The small project I made with hovering: https://www.dropbox.com/s/wq4ggufbxrfcvb8/HoverButton.zip?dl=0

RuuddR
  • 941
  • 4
  • 13
  • 25

1 Answers1

2

The best way to do this is probably to use a CardLayout which is already set up for good behavior.

Here's an MCVE.

mouseover panels MCVE screenshot

package mcve;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.imageio.*;
import java.net.*;
import java.io.*;

public class MouseoverPanels {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(MouseoverPanels::new);
    }

    MouseoverPanels() {
        JFrame frame = new JFrame("Mouseover Panels");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel buttons = new JPanel();
        buttons.setBackground(Color.WHITE);

        buttons.add(new MouseoverPanel("Queen",  QUEEN_IMG));
        buttons.add(new MouseoverPanel("King",   KING_IMG));
        buttons.add(new MouseoverPanel("Rook",   ROOK_IMG));
        buttons.add(new MouseoverPanel("Knight", KNIGHT_IMG));
        buttons.add(new MouseoverPanel("Pawn",   PAWN_IMG));

        frame.setContentPane(buttons);

        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    class MouseoverPanel extends JPanel {
        static final String BUTTON_KEY = "BUTTON";
        static final String ICON_KEY   = "ICON";

        final CardLayout layout = new CardLayout();

        MouseoverPanel(String text, Image img) {
            setLayout(layout);
            setOpaque(false);

            JButton button = new JButton(text);
            button.setBorderPainted(false);
            button.setOpaque(false);

            JLabel icon = new JLabel(new ImageIcon(img));
            icon.setOpaque(false);

            add(button, BUTTON_KEY);
            add(icon,   ICON_KEY);

            layout.show(this, ICON_KEY);

            MouseoverListener ml = new MouseoverListener();

            addMouseListener(ml);
            button.addMouseListener(ml);
            icon.addMouseListener(ml);
        }

        class MouseoverListener extends MouseAdapter {
            @Override
            public void mouseEntered(MouseEvent e) {
                recomputeView(e);
            }
            @Override
            public void mouseExited(MouseEvent e) {
                recomputeView(e);
            }
            void recomputeView(MouseEvent e) {
                Component comp = e.getComponent();
                Point     loc  = SwingUtilities.convertPoint(comp, e.getPoint(), MouseoverPanel.this);
                String    key  = contains(loc) ? BUTTON_KEY : ICON_KEY;
                layout.show(MouseoverPanel.this, key);
            }
        }
    }

    static final Image QUEEN_IMG,
                       KING_IMG,
                       ROOK_IMG,
                       KNIGHT_IMG,
                       PAWN_IMG;
    static {
        try {
            // source for sprite sheet: https://stackoverflow.com/a/19209651/2891664
            BufferedImage sprites = ImageIO.read(new URL("https://i.stack.imgur.com/memI0.png"));
            int n = 64;
            QUEEN_IMG  = sprites.getSubimage(0 * n, 0, n, n);
            KING_IMG   = sprites.getSubimage(1 * n, 0, n, n);
            ROOK_IMG   = sprites.getSubimage(2 * n, 0, n, n);
            KNIGHT_IMG = sprites.getSubimage(3 * n, 0, n, n);
            PAWN_IMG   = sprites.getSubimage(4 * n, 0, n, n);
        } catch (IOException x) {
            throw new UncheckedIOException(x);
        }
    }
}

I think the performance hit you're seeing is probably because calling setVisible may cause revalidate to be called, which invokes the layout manager, but it's hard to say for sure, especially without a proper MCVE (see).


Here's another example which does this via custom painting only. An approach like this should be easier to animate. This might not normally be feasible, except that the only "real" component we're dealing with here is the text itself.

example with only custom painting

package mcve;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.imageio.*;
import java.net.*;
import java.io.*;
import java.util.*;

public class MouseoverPanels2 {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(MouseoverPanels2::new);
    }

    MouseoverPanels2() {
        JFrame frame = new JFrame("Mouseover Panels");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel buttons = new JPanel();
        buttons.setBackground(Color.WHITE);

        buttons.add(new MouseoverLabel("Queen",  QUEEN_IMG,  QUEEN_IMG_H));
        buttons.add(new MouseoverLabel("King",   KING_IMG,   KING_IMG_H));
        buttons.add(new MouseoverLabel("Rook",   ROOK_IMG,   ROOK_IMG_H));
        buttons.add(new MouseoverLabel("Knight", KNIGHT_IMG, KNIGHT_IMG_H));
        buttons.add(new MouseoverLabel("Pawn",   PAWN_IMG,   PAWN_IMG_H));

        frame.setContentPane(buttons);

        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    class MouseoverLabel extends JComponent {
        final JLabel label;
        final Image image, hover;

        final Dimension imageMaxSize;

        boolean isHovering = false;

        MouseoverLabel(String text, Image image, Image hover) {
            setLayout(new BorderLayout());
            setOpaque(false);

            this.image = Objects.requireNonNull(image);
            this.hover = Objects.requireNonNull(hover);

            imageMaxSize =
                new Dimension(Math.max(image.getWidth(this), hover.getWidth(this)),
                              Math.max(image.getHeight(this), hover.getHeight(this)));

            label = new JLabel(text);
            label.setForeground(Color.RED);
            label.setOpaque(false);
            label.setHorizontalAlignment(JLabel.CENTER);
            add(label, BorderLayout.CENTER);

            MouseoverListener ml = new MouseoverListener();

            addMouseListener(ml);
            label.addMouseListener(ml);
        }

        private Dimension getMax(Dimension size) {
            size.width = Math.max(size.width, imageMaxSize.width);
            size.height = Math.max(size.height, imageMaxSize.height);
            return size;
        }

        @Override
        public Dimension getPreferredSize() {
            return getMax(super.getPreferredSize());
        }
        @Override
        public Dimension getMinimumSize() {
            return getMax(super.getMinimumSize());
        }
        @Override
        public Dimension getMaximumSize() {
            return getMax(super.getMaximumSize());
        }

        @Override
        protected void paintChildren(Graphics g) {
            // Note that the label is always "visible",
            // so it will e.g. receive mouse events even
            // while we aren't painting it.
            // If the label needs to receive, say, mouse
            // clicks, then you need to check isHovering
            // in the mouse click listener.
            // If the label is a JButton, then you could
            // call button.setEnabled(isHovering) in the
            // MouseoverListener.
            if (isHovering) {
                paintImage(g, hover);
                super.paintChildren(g);
            } else {
                paintImage(g, image);
            }
        }

        private void paintImage(Graphics g, Image image) {
            int w = image.getWidth(this);
            int h = image.getHeight(this);
            int x = (getWidth() - w) / 2;
            int y = (getHeight() - h) / 2;
            g.drawImage(image, x, y, w, h, this);
        }

        class MouseoverListener extends MouseAdapter {
            @Override
            public void mouseEntered(MouseEvent e) {
                recomputeView(e);
            }
            @Override
            public void mouseExited(MouseEvent e) {
                recomputeView(e);
            }
            void recomputeView(MouseEvent e) {
                Component comp = e.getComponent();
                Point     loc  = SwingUtilities.convertPoint(comp, e.getPoint(), MouseoverLabel.this);
                isHovering     = contains(loc);
                repaint();
            }
        }
    }

    static final Image QUEEN_IMG,
                       QUEEN_IMG_H,
                       KING_IMG,
                       KING_IMG_H,
                       ROOK_IMG,
                       ROOK_IMG_H,
                       KNIGHT_IMG,
                       KNIGHT_IMG_H,
                       PAWN_IMG,
                       PAWN_IMG_H;
    static {
        try {
            // source for sprite sheet: https://stackoverflow.com/a/19209651/2891664
            BufferedImage sprites = ImageIO.read(new URL("https://i.stack.imgur.com/memI0.png"));
            int n = 64;
            QUEEN_IMG    = sprites.getSubimage(0 * n, 0, n, n);
            QUEEN_IMG_H  = sprites.getSubimage(0 * n, n, n, n);
            KING_IMG     = sprites.getSubimage(1 * n, 0, n, n);
            KING_IMG_H   = sprites.getSubimage(1 * n, n, n, n);
            ROOK_IMG     = sprites.getSubimage(2 * n, 0, n, n);
            ROOK_IMG_H   = sprites.getSubimage(2 * n, n, n, n);
            KNIGHT_IMG   = sprites.getSubimage(3 * n, 0, n, n);
            KNIGHT_IMG_H = sprites.getSubimage(3 * n, n, n, n);
            PAWN_IMG     = sprites.getSubimage(4 * n, 0, n, n);
            PAWN_IMG_H   = sprites.getSubimage(4 * n, n, n, n);
        } catch (IOException x) {
            throw new UncheckedIOException(x);
        }
    }
}
Radiodef
  • 37,180
  • 14
  • 90
  • 125
  • I've played a bit around with your code and I found out that when I make it look like the example I had, that it doesn't occur. You can find my edits in the main post. – RuuddR Apr 02 '18 at 20:10
  • Well, my suggestions would be 1. try using a `CardLayout` in your actual project and/or 2. see if you can reproduce the slowness in an MCVE. You can still use `CardLayout` to do something more similar to what you have in your modification of my example. You could, say, put the label in a panel which paints the background image, or use a nested `OverlayLayout`. The point of `CardLayout` is just that it precomputes all the sizes and you can switch components out without invoking `setVisible`, `revalidate`, etc. – Radiodef Apr 02 '18 at 20:21
  • I really would like to use the CardLayout. The problem is that I want to make a hover transition, so the icon turns from the starting color to dark and I want the text to appear, this should all go gradually. I don’t think that this is possible using the CardLayout – RuuddR Apr 02 '18 at 20:42
  • See my edit for another idea which might be easier to animate. – Radiodef Apr 02 '18 at 21:27
  • I checked out your edit and I tried to edit it to my needs again. I made a small project out of it and only add the stuff I need, like transparency and a background. The weird thing is, that in that small project it's not slow, while in my bigger project, it is slow. in the main post I added the small project, which isn't slow, not sure if you can do anything with it. – RuuddR Apr 03 '18 at 17:55
  • This is another guess, but if you're doing image scaling in `paintComponent` in the real code, it could be the culprit. You can do scaling with the `drawImage` methods and they should do caching, or you could only scale the image whenever the component's size changes. It's hard for me to help with code that I can't look at. – Radiodef Apr 03 '18 at 18:50
  • To be honest, I am doing image scaling in the paintComponent to make the background image always fit the frame. I disabled the background image and it didn't slow it down anymore, so that must be the answer. Is there any way I can do the scaling in a different thread or anything like that? – RuuddR Apr 04 '18 at 18:52
  • The `drawImage` overloads which accept a width and height will do scaling ([for example](https://docs.oracle.com/javase/9/docs/api/java/awt/Graphics.html#drawImage-java.awt.Image-int-int-int-int-java.awt.image.ImageObserver-)). You could also do caching yourself by just having a private field `BufferedImage img;` and then only recreating it if `getWidth() != img.getWidth() || getHeight() != img.getHeight()`, but I don't think there's any point to that unless you want to scale the image with some specific option. – Radiodef Apr 04 '18 at 19:06
  • The documentation for `drawImage` seems to imply that the scaling could be done on a background thread, but I don't think it's that important. The most important thing is just to only rescale the image when the size changes. `drawImage` should do that already (sun/OpenJDK does), except for [this overload](https://docs.oracle.com/javase/9/docs/api/java/awt/Graphics.html#drawImage-java.awt.Image-int-int-int-int-int-int-int-int-java.awt.image.ImageObserver-) and the other one like it. – Radiodef Apr 04 '18 at 19:21
  • I used `drawImage` just like that before and then it did make the hovering slow. Now I did it like you said, only when the width and height don't match, the hovering isn't slow then. The problem is that whenever I now hover over a component with Opaque, that the background of that component gets white and not like the background. So it looks like that it HAS to redraw the background to find out what it has to draw 'behind' the components with Opaque. – RuuddR Apr 04 '18 at 20:10
  • You can draw the image every time, just don't recreate it unless the size changes. – Radiodef Apr 04 '18 at 20:27
  • I had a method that changes the brightness of an image. I used that method inside the `paintComponent` method and I guess that method takes a lot of time to complete, because when I moved the call to the brightness method inside the constructor, the slow hovering was over. – RuuddR Apr 05 '18 at 20:44