Crashtestdummy
Crashtestdummy

Reputation: 9

Can I use BoxLayout on a JLayeredPane to space items on each layer individually?

I am creating Uno as my final project in my CompSci class.
The cards in playerHandPanel are made of JButtons. The buttons should be displayed sequentially in the order they were dealt, but also should be overlapping to fit more than the starting 7 cards in the panel.

My teacher suggested I use JLayeredPane instead of JPanel as the container because items can be put on different layers.
It didn't work because I was using BoxLayout to auto-place them with LINE_AXIS and that layout completely ignores the layers and spaces them as if it was a normal JPanel.

BoxLayout ignoring the layers:
BoxLayout ignoring the layers

Rather, they should overlap like this:
They should overlap like this

This is how playerHandPanel is set up:

    public void setupImgs()
    {
        //gets the dealt hand and sets up the buttons/imgs
        for (int i = 0; i < deck.getPlayerHand().size(); i++)
        {
            setupHand(new JButton(deck.getHandImgs().get(i)), i);
        }
    }
    public void setupHand(JButton img, int index)
    {
        //TODO allow more than 7 cards in hand
        //TODO jlayeredpane to allow overlap by using layers
        //boxlayout puts them in sequential but ignores layers >:(

        playerHandPanel = new JLayeredPane();
        playerHandPanel.setBounds(250, 350, 1400, 300);
        playerHandPanel.setBackground(Color.GRAY);
        playerHandPanel.setLayout(new BoxLayout(playerHandPanel, BoxLayout.LINE_AXIS));
        //using the 

        if (index == 0)
        {
            //playerHand1
            ph1 = new JButton("", img.getIcon());
            ph1.setSize(150, 240);
            ph1.setLocation(0, 0);
            ph1.setMargin(new Insets(1,1,1,1));
            ph1.addActionListener(this);
            ph1.addMouseListener(this);//moves up when card focused
            playerHandPanel.add(ph1, JLayeredPane.DEFAULT_LAYER);
            //placed on bottom-most layer
        }

        if (index == 1)
        {
            ph2 = new JButton("", img.getIcon());
            ph2.setSize(150, 240);
            ph2.setLocation(20, 0);
            ph2.setMargin(new Insets(1,1,1,1));
            ph2.addActionListener(this);
            ph2.addMouseListener(this);
            playerHandPanel.add(ph2, JLayeredPane.PALETTE_LAYER);
            //placed on second lowest layer

        }
        
        if (index == 2)
        {
            ph3 = new JButton("", img.getIcon());
            ph3.setSize(150, 240);
            ph3.setLocation(50, 0);
            ph3.setMargin(new Insets(1,1,1,1));
            ph3.addActionListener(this);
            ph3.addMouseListener(this);
            playerHandPanel.add(ph3, JLayeredPane.MODAL_LAYER);
            //placed on third lowest layer
        }
        //continued to 7...
        window.add(playerHandPanel);
        playerHandPanel.setVisible(true);
    }

I tried using one or two other layouts, like but couldn't really figure out how they worked. In the end I just went back to BoxLayout. I tried GroupLayout because I thought I could group cards a certain way across layers. I don't really know how to explain it, but it doesn't really matter because it didn't work.
I tried to draw it though

Upvotes: 0

Views: 86

Answers (2)

Crashtestdummy
Crashtestdummy

Reputation: 9

This is what I needed, thanks to @camickr for the comment. This is very simple and as I said, I don't have much time to try something completely new.
"Check out the Overlap Layout. Think you can also use the FlowLayout with a negative horizontal gap. You will also need to override the isOptimizedDrawingEnabled() to return false on the panel." -@camickr

I will also be adding a rule where the Hgap changes when cards are added to the hand.

Upvotes: -1

Hovercraft Full Of Eels
Hovercraft Full Of Eels

Reputation: 285405

One possible solution is to create your own layout manager. For instance, if you wanted to layout a bunch of JLabels that represented Uno playing cards so that they overlap each other, the last one added placed above the prior cards and slightly to the right, you could create a layout that looks something like

enter image description here

UnoCardHandLayout.java

import java.awt.*;

// layout manager that lays out Uno cards in a hand
// the cards are JLabels that are added to a JPanel
// and are placed one over the other, but leaving a GAP between them
public class UnoCardHandLayout implements LayoutManager {
    private static final int DEFAULT_GAP = 60;
    private int gap; // gap between cards

    public UnoCardHandLayout() {
        this(DEFAULT_GAP);
    }

    public UnoCardHandLayout(int gap) {
        this.gap = gap;
    }

    @Override
    public void addLayoutComponent(String name, Component comp) {
    }

    @Override
    public void removeLayoutComponent(Component comp) {
    }

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        int width = 0;
        int height = 0;
        for (Component comp : parent.getComponents()) {
            Dimension pref = comp.getPreferredSize();
            width = Math.max(width, pref.width);
            height = Math.max(height, pref.height);
        }
        Insets insets = parent.getInsets();
        width += insets.left + insets.right + gap;
        height += insets.top + insets.bottom + gap;
        return new Dimension(width, height);
    }

    @Override
    public Dimension minimumLayoutSize(Container parent) {
        return preferredLayoutSize(parent);
    }

    @Override
    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
        int x = insets.left;
        int y = insets.top;
        for (int i = parent.getComponentCount() - 1; i >= 0; i--) {
            Component comp = parent.getComponent(i);
            Dimension pref = comp.getPreferredSize();
            comp.setBounds(x, y, pref.width, pref.height);
            x += gap;
        }

    }
}

This layout manager adds components, starting with the last component in the component array (the first one added), and adding a gap of gap distance between the next overlying component. This can be used with a GamePanel JPanel that holds the Uno card "hand"

GamePanel.java

import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;

import javax.swing.*;
import javax.swing.border.*;

public class GamePanel extends JPanel {
    private List<JLabel> cardLabels = new ArrayList<>();
    private static final int MIN_PREF_WIDTH = 1200;
    private static final int MIN_PREF_HEIGHT = 300;
    private static final Color BKG_COLOR = new Color(0, 80, 0); // game table dark green
    private static final int CARD_GAP = 60;

    public GamePanel() {
        setLayout(new UnoCardHandLayout(CARD_GAP)); // !! important
        setBackground(BKG_COLOR);
        Border border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.WHITE), "Game Panel",
                TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, Color.WHITE);
        setBorder(border);
    }

    @Override
    public Dimension getPreferredSize() {
        // return a size that is at least the minimum preferred size.
        Dimension superSize = super.getPreferredSize();
        int width = Math.max(superSize.width, MIN_PREF_WIDTH);
        int height = Math.max(superSize.height, MIN_PREF_HEIGHT);
        return new Dimension(width, height);
    }

    public void addUnoCard(JLabel cardLabel) {
        cardLabels.add(cardLabel);
        add(cardLabel, 0); // add to the top of the z-order
        revalidate();
        repaint();
    }
}

When an uno card is added, by calling addUnoCard(...), the card is added at the 0th component position, to the top of the z-order. Then revalidate and repaint are called to tell the layout managers to layout components and to tell the component to repaint, removing dirty pixels.

UnoDeckPanel.java

import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.*;

public class UnoDeckPanel extends JPanel {
    public static final String CARD_DROP = "card drop";
    private static final int GAP = 10;
    private Icon emptyIcon;
    private List<Icon> unoDeckIcons;
    private List<Icon> cardPileIcons = new ArrayList<>();
    private JLabel deckLabel = null;
    private JLabel cardPileLabel = null;

    public UnoDeckPanel(Icon backIcon, Icon emptyIcon, List<Icon> icons) {
        this.unoDeckIcons = new ArrayList<>(icons);
        this.emptyIcon = emptyIcon;
        Collections.shuffle(unoDeckIcons);
        JPanel innerPanel = new JPanel();
        innerPanel.setLayout(new GridLayout(1, 0, GAP, GAP));
        innerPanel.setBorder(BorderFactory.createEmptyBorder(GAP, 20 * GAP, GAP, 20 * GAP));
        deckLabel = new JLabel(backIcon);
        innerPanel.add(deckLabel);
        cardPileLabel = new JLabel(emptyIcon);
        innerPanel.add(cardPileLabel);

        setLayout(new GridBagLayout());
        add(innerPanel);

        deckLabel.addMouseListener(new UnoDeckListener());
        CardPileListener cardPileListener = new CardPileListener();
        cardPileLabel.addMouseListener(cardPileListener);
        cardPileLabel.addMouseMotionListener(cardPileListener);
    }

    private class UnoDeckListener extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent e) {
            if (unoDeckIcons.isEmpty()) {
                JOptionPane.showMessageDialog(UnoDeckPanel.this, "Deck is empty");
                return;
            }
            Icon icon = unoDeckIcons.remove(0);
            cardPileLabel.setIcon(icon);
            cardPileIcons.add(icon);

            if (unoDeckIcons.isEmpty()) {
                deckLabel.setIcon(emptyIcon);
            }
        }
    }

    private class CardPileListener extends MouseAdapter {
        private Icon poppedIcon = null;
        JLabel poppedLabel = null;

        public void mousePressed(MouseEvent e) {
            if (cardPileLabel.getIcon() != emptyIcon) {
                // first get the icon from the cardPileLabel, the top "card"
                poppedIcon = cardPileIcons.remove(cardPileIcons.size() - 1);
                // set the new top card from the icon underneath the one removed
                cardPileLabel
                        .setIcon(cardPileIcons.isEmpty() ? emptyIcon : cardPileIcons.get(cardPileIcons.size() - 1));

                // get the glass pane and add the popped label
                JComponent glassPane = (JComponent) UnoDeckPanel.this.getRootPane().getGlassPane();
                glassPane.setVisible(true);
                glassPane.setLayout(null); // null so we can drag the label
                poppedLabel = new JLabel(poppedIcon);
                glassPane.add(poppedLabel);
                poppedLabel.setSize(poppedLabel.getPreferredSize());

                // set the location of the popped label, centered on the mouse position
                Point currentPoint = e.getLocationOnScreen();
                Point glassPaneLocation = glassPane.getLocationOnScreen();
                int halfWidth = poppedLabel.getWidth() / 2;
                int halfHeight = poppedLabel.getHeight() / 2;
                Point p = new Point(currentPoint.x - glassPaneLocation.x - halfWidth,
                        currentPoint.y - glassPaneLocation.y - halfHeight);

                poppedLabel.setLocation(p);
                glassPane.revalidate();
                glassPane.repaint();
            }
        }

        public void mouseReleased(MouseEvent e) {
            if (poppedIcon != null) {
                // get the glass pane and remove the popped label
                JComponent glassPane = (JComponent) UnoDeckPanel.this.getRootPane().getGlassPane();
                glassPane.removeAll();
                glassPane.setVisible(false);

                // notify anyone listening to the card drop property that a card has been
                // dropped
                // and pass in the icon that was dropped
                UnoDeckPanel.this.firePropertyChange(CARD_DROP, null, poppedIcon);

                // reset things
                poppedIcon = null;
                poppedLabel = null;
            }
        }

        public void mouseDragged(MouseEvent e) {
            if (poppedIcon != null) {
                JComponent glassPane = (JComponent) UnoDeckPanel.this.getRootPane().getGlassPane();
                Point currentPoint = e.getLocationOnScreen();
                Point glassPaneLocation = glassPane.getLocationOnScreen();
                int halfWidth = poppedLabel.getWidth() / 2;
                int halfHeight = poppedLabel.getHeight() / 2;
                Point p = new Point(currentPoint.x - glassPaneLocation.x - halfWidth,
                        currentPoint.y - glassPaneLocation.y - halfHeight);
                poppedLabel.setLocation(p);
                glassPane.revalidate();
                glassPane.repaint();
            }
        }
    }

}

It holds an Uno deck of cards and a discard pile. When the deck is clicked, a card is removed and added to and displayed in the discard pile. When the discard pile is clicked, the current card displayed is kicked up into the glass pane and allowed to be dragged by the user's mouse. When released, any listeners are notified of the card (the image icon) that was released so that parent component can place the card into the game panel.

UnoMainGui.java

import java.awt.BorderLayout;
import java.awt.MouseInfo;
import java.awt.Point;
import java.beans.*;
import java.util.List;
import javax.swing.*;

// main JPanel that holds both the deck panel and the game panel
public class UnoMainGui extends JPanel {
    private UnoDeckPanel deckPanel;
    private GamePanel gamePanel = new GamePanel();

    public UnoMainGui(List<Icon> icons) {
        deckPanel = new UnoDeckPanel(UnoMain.UNO_BACK_ICON, UnoMain.UNO_EMPTY_ICON, icons);

        setLayout(new BorderLayout(2, 2));
        add(deckPanel, BorderLayout.PAGE_START);
        add(gamePanel, BorderLayout.CENTER);

        // if we drop a card from the deck panel, add it to the game panel
        // this does not check for invalid drops (e.g., off of the game panel)
        deckPanel.addPropertyChangeListener(UnoDeckPanel.CARD_DROP, new DeckPanelListener());
    }

    // user has dropped a card dragged off the deck
    private class DeckPanelListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            Icon icon = (Icon) evt.getNewValue();
            if (icon == null) {
                return;

            }
            JLabel cardLabel = new JLabel(icon);
            cardLabel.setSize(cardLabel.getPreferredSize());

            // get the location of the mouse relative to the game panel's coordinate system
            Point mousePointOnScreen = MouseInfo.getPointerInfo().getLocation();
            Point gamePanelLocationOnScreen = gamePanel.getLocationOnScreen();
            int halfWidth = cardLabel.getWidth() / 2;
            int halfHeight = cardLabel.getHeight() / 2;
            Point p = new Point(mousePointOnScreen.x - gamePanelLocationOnScreen.x - halfWidth,
                    mousePointOnScreen.y - gamePanelLocationOnScreen.y - halfHeight);
            cardLabel.setLocation(p);

            gamePanel.addUnoCard(cardLabel);
        }
    }
}

This gets the GUI set up and started. It adds a PropertyChangeListener onto the deck panel so it can be notified if a card was dragged and released. Within the listener, the card is deposited into the game panel.

Finally, UnoMain.java

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.*;

public class UnoMain {
    public static final String UNO_CARD_SHEET = "https://upload.wikimedia.org/wikipedia/commons/"
            + "thumb/9/95/UNO_cards_deck.svg/2389px-UNO_cards_deck.svg.png";
    public static final int COLUMNS = 14;
    public static final int ROWS = 8;
    public static Icon UNO_BACK_ICON = null;
    public static Icon UNO_EMPTY_ICON = null;

    public static void main(String[] args) {
        // get a Uno card sprite sheet from a public source
        BufferedImage unoImage = null;
        try {
            URL unoUrl = new URL(UNO_CARD_SHEET);
            unoImage = ImageIO.read(unoUrl);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        // and then subdivide the sprite sheet into individual icons
        double width = unoImage.getWidth() / (double) COLUMNS;
        double height = unoImage.getHeight() / (double) ROWS;

        Icon[][] icons = new Icon[ROWS][COLUMNS];

        for (int row = 0; row < ROWS; row++) {
            for (int col = 0; col < COLUMNS; col++) {
                BufferedImage img = unoImage.getSubimage((int) (col * width), (int) (row * height), (int) width,
                        (int) height);
                Icon icon = new ImageIcon(img);
                icons[row][col] = icon;
            }
        }

        // put some of them into an ArrayList
        List<Icon> unoDeckIconList = new ArrayList<>();
        for (int row = 0; row < ROWS / 2; row++) {
            for (int col = 0; col < COLUMNS - 1; col++) {
                unoDeckIconList.add(icons[row][col]);
            }
        }

        // back of a UNO card
        UNO_BACK_ICON = icons[0][COLUMNS - 1];

        // empty card
        BufferedImage emptyImage = new BufferedImage((int) width, (int) height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = emptyImage.createGraphics();
        g2d.setColor(Color.BLACK);
        g2d.fillRect(0, 0, (int) width, (int) height);
        g2d.dispose();
        UNO_EMPTY_ICON = new ImageIcon(emptyImage);

        // create the Swing GUI on the event thread
        SwingUtilities.invokeLater(() -> {
            UnoMainGui mainGui = new UnoMainGui(unoDeckIconList);
            JFrame frame = new JFrame("UNO Card Sheet");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new JScrollPane(mainGui));
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });

    }
}

Upvotes: 2

Related Questions