0

I have a PopUp class that expands and contracts like a pop-up. It extends JPanel.

I have overridden the typical visibility methods for the JPanel to choose whether the PopUp object should be drawn. The JPanel should be visible only when the pop-up is fully expanded.

However, this is the part that does not work.

Here is the relevant PopUp class code. I added some comments that hopefully help:

public class PopUp extends JPanel {

    /**
     * Expanded x coordinate
     */
    private double x;
    /**
     * Expanded y coordinate
     */
    private double y;
    /**
     * Expanded width value
     */
    private double width;
    /**
     * Expanded height value
     */
    private double height;

    /**
     * Number of steps until fully expanded
     */
    private int steps;
    /**
     * This divided by steps is the percentage the pop-up is expanded
     */
    private int expansionStage = 0;

    /**
     * Whether or not the pop-up is expanding
     */
    private boolean isExpanding = false;
    /**
     * Whether or not the pop-up is visible
     */
    private boolean visible;

    /**
     * Color of the pop-up
     */
    private Color color;

    /**
     * The rectangle that represents the bounds of the pop-up
     */
    private Rectangle2D popUp;

    /**
     * The currently used transform for the pop-up
     */
    private AffineTransform trans;

    /**
     * Initializes a newly created {@code PopUp} with a uniform color
     * @param x                 The x coordinate of the expanded pop-up
     * @param y                 The y coordinate of the expanded pop-up
     * @param w                 The width of the expanded pop-up
     * @param h                 The height of the expanded pop-up
     * @param expansionSteps    The number of steps until fully expanded
     * @param popUpColor        The color of the pop-up
     */
    public PopUp(double x, double y, double w, double h, int expansionSteps, Color popUpColor) {
        this.x = x;
        this.y = y;
        width = w;
        height = h;
        color = popUpColor;
        steps = expansionSteps;
        this.borderWidth = 0;
        this.borderColor = null;
        popUp = new Rectangle2D.Double(0, 0, width, height);
        setBounds((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h));
        trans = new AffineTransform();
        //Centers the rectangle pop-up at the center of the given rectangle made by the given x, y, width, and height
        trans.translate(x + width/2 * (1 - (double) expansionStage/steps), y + height/2 * (1 - (double) expansionStage/steps));
        //Scales the rectangle based on the percentage it is expanded
        trans.scale((double) expansionStage/steps, (double) expansionStage/steps);
    }

    /**
     * Draws the pop-up
     * @param g     Graphics object from paintComponent
     */
    public final void draw(Graphics g) {
        //Expands pop-up one step
        if(isExpanding && visible)
            expansionStage = Math.min(expansionStage + 1, steps);
        //Contracts pop-up one step
        else if(visible)
            expansionStage = Math.max(expansionStage - 1, 0);
        //Resets pop-up size to 0
        else
            expansionStage = 0;
        if(visible) {
            //Sets the visibility of the JPanel to true if the pop-up is fully expanded (false otherwise)
            super.setVisible(expansionStage/steps == 1);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            AffineTransform trans = new AffineTransform();
            //Centers the rectangle pop-up at the center of the given rectangle made by the given x, y, width, and height
            trans.translate(x + width/2 * (1 - (double) expansionStage/steps), y + height/2 * (1 - (double) expansionStage/steps));
            //Scales the rectangle based on the percentage it is expanded
            trans.scale((double) expansionStage/steps, (double) expansionStage/steps);
            this.trans = trans;
            g2d.setColor(color);
            Shape transformed = trans.createTransformedShape(popUp);
            g2d.fill(transformed);
        }
        else
            super.setVisible(false);
    }

    /**
     * Sets whether the pop-up is expanding or not
     * @param expanding    Whether or not the pop-up should expand
     */
    public final void setExpanding(boolean expanding) {
        isExpanding = expanding;
    }

    /**
     * Returns whether or not the pop-up is expanding
     * @return Whether or not the pop-up is expanding
     */
    public final boolean getExpanding() {
        return isExpanding;
    }

    /**
     * Returns the percentage that the pop-up has expanded
     * @return The percentage that the pop-up has expanded
     */
    public final double percentageExpanded() {
        return (double) expansionStage/steps;
    }

    /**
     * Different than JPanel.setVisible(boolean visible) in that it
     * only draws the PopUp if this is true, and the JPanel is visible
     * only when this is true and the popUp is expanded
     * @param visible   Whether or not the pop-up should be visible
     */
    @Override
    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    /**
     * Different than JPanel.isVisible() in that it
     * only draws the PopUp if this is true, and the JPanel is visible
     * only when this is true and the popUp is expanded
     * @return  Whether or not the pop-up should be visible
     */
    @Override
    public boolean isVisible() {
        return visible;
    }

    public boolean jPanelIsVisible() {
        return super.isVisible();
    }

}

I set it up by creating one. Then, I then add it to the main JPanel and set its visibility to true.

In the paintComponent() method for the main JPanel, I put a call to PopUp.draw(g).

Finally, I have PopUp.setExpanding(true) when I want it to expand and PopUp.setExpanding(false) when I want it to contract.

Let me know if any other information is required.

Update:

I am planning to use a modified version of MadProgrammer's version of my PopUp class, but I thought I would let you all know what the real problem was.

MadProgrammer had the right idea when he was thinking the visibility was the problem. When I was using super.setVisible(), it referred to my isVisible() method for my PopUp, which was unfortunate.

Gigi Bayte 2
  • 838
  • 1
  • 8
  • 20
  • *"In the paintComponent() method for the main JPanel, I put a call to PopUp.draw(g)."* - Why? While would you need a panel to paint another panel, to me, this doesn't make sense and is counter intuitive of how a panel actually works – MadProgrammer Feb 18 '18 at 01:33
  • Over the years, I've developer a number of "popup" style APIs, some used plain old Windows (I wouldn't recommend it), but most relied heavily on the use of the `GlassPane`, as it provides a overlap layer over the top of the reset of the UI. For [example](https://stackoverflow.com/questions/28668955/how-to-set-the-position-of-glasspane-in-jframe/28669000#28669000), [example](https://stackoverflow.com/questions/26473308/display-a-message-on-the-screen/26474379#26474379) – MadProgrammer Feb 18 '18 at 01:43
  • [example](https://stackoverflow.com/questions/15961472/how-can-i-paint-in-an-specific-jpanel-when-more-than-one-in-same-frame-java/15961655#15961655) and [example](https://stackoverflow.com/questions/19083355/jpanel-setlocation/19083537#19083537). I even wrote a "message box" popup API for the express purpose of displaying short lived messages to the user, which was fully animated, allowed for automatic, manual and/or program dismissal. It also allowed for expansion for new message types of over time and was very comprehensive. It all used the `GlassPane` as the primary display surface – MadProgrammer Feb 18 '18 at 01:46
  • I have repaint() called for the main JPanel by a timer every 10 ms. I use the same graphics context for any drawing that I do. Based on what you said, It seems I am not drawing in an efficient way. So, I will try and remedy that for the next project I do. @MadProgrammer – Gigi Bayte 2 Feb 18 '18 at 01:46
  • @MadProgrammer That's pretty cool! And I will definitely look at your classes. However, I feel like the PopUp class is nearly complete and just has the one minor aforementioned error. So, if possible, I would like to stick to this class at least for this project if you don't mind. :) – Gigi Bayte 2 Feb 18 '18 at 01:48
  • *"I have repaint() called for the main JPanel by a timer every 10 ms"* - Okay, is probably slightly incorrect. The `Timer` should be used to change the state of the `Popup` which should then trigger a paint cycle. This makes the painting process independent of the update process, as painting can occur for any number of reasons – MadProgrammer Feb 18 '18 at 04:14
  • You "key" problem seems to be `super.setVisible(expansionStage/steps == 1);`, which is setting the visible state to `false` when `expansionStage/steps` is NOT equal to `1` - I'd also be concerned with extending from `JPanel`, as it's not providing any additional functionality and the animation should centred around the component itself, as it needs to decide when to start and stop it – MadProgrammer Feb 18 '18 at 04:22
  • @MadProgrammer The painting and updating should be separate. Got it! – Gigi Bayte 2 Feb 18 '18 at 04:48

1 Answers1

0

So, after some digging around, I think you biggest issue is with the "visibility" concept.

Namely, super.setVisible(expansionStage/steps == 1);, which will make the component invisible while expansionStage/steps is not equal to 1.

I scrapped the concept of visibility and removed the inheritance from JPanel, as it's not adding any benefit and causing no end of issues.

I also moved the animation cycle to the class itself and used a update method to update the state independently of the paint process, as painting can occur at any time for any number of reasons, which could interrupt with the animation.

I also added a couple of methods which provide information about whether the class is fully expanded or collapsed, which provides a trigger point for determining when the animation should be stopped.

In my mind (scary place), you should have a "state" variable which contains expanded, collapsed, expanding and collapsing, from which you can make determinations about what should occur at different times. It also means you could reverse the animation mid cycle if you wanted to.

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

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

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private PopUp popUp;

        public TestPane() {
            popUp = new PopUp(10, 10, 180, 180, 10, Color.yellow);

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    popUp.animate(TestPane.this);
                }
            });
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            popUp.draw(g);
            g2d.dispose();
        }

    }

    public class PopUp {

        /**
         * Expanded x coordinate
         */
        private double x;
        /**
         * Expanded y coordinate
         */
        private double y;
        /**
         * Expanded width value
         */
        private double width;
        /**
         * Expanded height value
         */
        private double height;

        /**
         * Number of steps until fully expanded
         */
        private int steps;
        /**
         * This divided by steps is the percentage the pop-up is expanded
         */
        private int expansionStage = 0;

        /**
         * Whether or not the pop-up is expanding
         */
        private boolean isExpanding = false;
        /**
         * Whether or not the pop-up is visible
         */
//      private boolean visible;

        /**
         * Color of the pop-up
         */
        private Color color;

        /**
         * The rectangle that represents the bounds of the pop-up
         */
        private Rectangle2D popUp;

        /**
         * The currently used transform for the pop-up
         */
        private AffineTransform trans;

        private Timer timer;
        private Component parent;

        /**
         * Initializes a newly created {@code PopUp} with a uniform color
         *
         * @param x The x coordinate of the expanded pop-up
         * @param y The y coordinate of the expanded pop-up
         * @param w The width of the expanded pop-up
         * @param h The height of the expanded pop-up
         * @param expansionSteps The number of steps until fully expanded
         * @param popUpColor The color of the pop-up
         */
        public PopUp(double x, double y, double w, double h, int expansionSteps, Color popUpColor) {
            this.x = x;
            this.y = y;
            width = w;
            height = h;
            color = popUpColor;
            steps = expansionSteps;
//          this.borderWidth = 0;
//          this.borderColor = null;
            popUp = new Rectangle2D.Double(0, 0, width, height);
//          setBounds((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h));
            trans = new AffineTransform();
            //Centers the rectangle pop-up at the center of the given rectangle made by the given x, y, width, and height
            trans.translate(x + width / 2 * (1 - (double) expansionStage / steps), y + height / 2 * (1 - (double) expansionStage / steps));
            //Scales the rectangle based on the percentage it is expanded
            trans.scale((double) expansionStage / steps, (double) expansionStage / steps);

            timer = new Timer(10, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    update();
                    parent.repaint();
                }
            });
        }

        public void animate(Component parent) {
            this.parent = parent;
            setExpanding(isCollapsed());
            timer.start();
        }

        public void update() {
            //Expands pop-up one step
            if (isExpanding) {
                if (!isExpanded()) {
                    expansionStage = Math.min(expansionStage + 1, steps);
                } else {
                    timer.stop();
                }
            } //Contracts pop-up one step
            else {
                if (!isCollapsed()) {
                    expansionStage = Math.max(expansionStage - 1, 0);
                } else {
                    timer.stop();
                }
            }
        }

        public boolean isCollapsed() {
            return (((double) expansionStage / (double) steps) == 0);
        }

        public boolean isExpanded() {
            return (((double) expansionStage / (double) steps) == 1);
        }

        /**
         * Draws the pop-up
         *
         * @param g Graphics object from paintComponent
         */
        public final void draw(Graphics g) {
//          if (visible) {
            //Sets the visibility of the JPanel to true if the pop-up is fully expanded (false otherwise)

            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            AffineTransform trans = new AffineTransform();
            //Centers the rectangle pop-up at the center of the given rectangle made by the given x, y, width, and height

            trans.translate(x + width / 2 * (1 - (double) expansionStage / steps), y + height / 2 * (1 - (double) expansionStage / steps));
            //Scales the rectangle based on the percentage it is expanded
            trans.scale((double) expansionStage / steps, (double) expansionStage / steps);
            this.trans = trans;
            g2d.setColor(color);
            Shape transformed = trans.createTransformedShape(popUp);
            g2d.fill(transformed);
//          } else {
////                setVisible(false);
//          }
        }

        /**
         * Sets whether the pop-up is expanding or not
         *
         * @param expanding Whether or not the pop-up should expand
         */
        public final void setExpanding(boolean expanding) {
            isExpanding = expanding;
//          setVisible(expanding);
        }

        /**
         * Returns whether or not the pop-up is expanding
         *
         * @return Whether or not the pop-up is expanding
         */
        public final boolean getExpanding() {
            return isExpanding;
        }

        /**
         * Returns the percentage that the pop-up has expanded
         *
         * @return The percentage that the pop-up has expanded
         */
        public final double percentageExpanded() {
            return (double) expansionStage / steps;
        }

//      /**
//       * Different than JPanel.setVisible(boolean visible) in that it only draws
//       * the PopUp if this is true, and the JPanel is visible only when this is
//       * true and the popUp is expanded
//       *
//       * @param visible Whether or not the pop-up should be visible
//       */
//      public void setVisible(boolean visible) {
//          this.visible = visible;
//      }
//
//      /**
//       * Different than JPanel.isVisible() in that it only draws the PopUp if this
//       * is true, and the JPanel is visible only when this is true and the popUp
//       * is expanded
//       *
//       * @return Whether or not the pop-up should be visible
//       */
//      public boolean isVisible() {
//          return visible;
//      }
    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I'll try this! However, my intention was actually to have the JPanel be invisible until the pop-up was fully expanded because I only wanted the JPanel to intercept mouse events when it was fully expanded. I'm sorry if I was unclear about this. (I removed the MouseListener stuff in an attempt to simplify.) – Gigi Bayte 2 Feb 18 '18 at 04:49
  • Okay so under the current implementation I have, I might consider putting into place a "expansion" listener, which provide notifications when it was fully expanded or collapsed, that we you could setup/take down the `MouseListener` in response to those events – MadProgrammer Feb 18 '18 at 05:31