Reputation: 9
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:
Rather, 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.
Upvotes: 0
Views: 86
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
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
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"
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.
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.
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.
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