Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resizing jPanels with animation

Tags:

java

swing

jpanel

I am developing an application with a FullScreen Panel, and sometimes I need to show a second Panel, in the right side of the screen.

To develop it I made a jPanel with AbsoluteLayout (from NetBeans) containing another two panels. I calculate each panel position and size relative to the screen resolution.

Basically, I have two "states". In the first the jPanel1 has width = 100% and the jPanel 2 has width = 0% (So I am not showing this panel). And the second state, when jPanel1 has width = 75% and jPanel2 has width = 25%.

To do these transitions I only recalculate each panel size and it works well, but I wish to do it with a single animation. With the jPanel2 "sliding" into the screen.

The following image explain it: Link to image

I tried some options to make it, but I had failed in every attempt. Basically, the "animation" is happening but the screen is refreshed only in the end. In my last attempt, I tried to use a swing Timer, my code is:

Variables:

-dataPanel: Major Panel, with AbsoluteLayout and containing jPanel1 and jPanel2

-visCons: AbsoluteConstraints for the jPanel1 position, when I am updating the witdh in the animation

-listCons: AbsoluteConstraints for the jPanel2 position, when I am updating the position in the animation.

public static void showListPanel() {
    timer = new Timer(1000, new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            int width = gd.getDisplayMode().getWidth();
            int height = gd.getDisplayMode().getHeight();
            int endVis =  width - (width/4 + 20);
            for (int i = width; i >= endVis; i--) {
                visCons.width = endVis;
                listCons.x = endVis;
                dataPanel.repaint();
                dataPanel.revalidate();
                try {
                    TimeUnit.MICROSECONDS.sleep(10);
                } catch (InterruptedException e2) {
                    // TODO Auto-generated catch block
                    e2.printStackTrace();
                }
            }
            timer.stop();
        }
    });
    timer.start();
}

I hope someone can tell me some alternatives to do it, or what I need to do to refresh the screen during the animation.

like image 421
fhenrique Avatar asked Mar 03 '14 00:03

fhenrique


2 Answers

Two years ago, I wrote a "LayoutAnimator" that can perform transitions between arbitrary layouts. Maybe you find it helpful:

import java.awt.Component;
import java.awt.Container;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;

import javax.swing.Timer;

/**
 * A class that may perform animations between different layouts
 * for containers.
 */
class LayoutAnimator
{
    /**
     * The map from containers to {@link LayoutAnimation LayoutAnimations}
     * that are currently running.
     */
    private static final Map<Container, LayoutAnimation> running =
        new IdentityHashMap<Container, LayoutAnimation>();

    /**
     * Execute the transition between the current layout of the given
     * container to the new layout. This method will animate the
     * contents of the given container, starting at its current state,
     * towards the state that is defined by setting the given layout
     * to the given container. The duration for the animation
     * (in seconds) may be specified. After the animation has finished,
     * the container will have the given layout.
     *
     * @param container The container
     * @param newLayout The new layout
     * @param durationS The duration, in seconds.
     */
    public static synchronized void execute(
        Container container, LayoutManager newLayout, double durationS)
    {
        // If there is already a LayoutAnimation running for the
        // container, cancel it and remove it from the map
        LayoutAnimation runningLayoutAnimtion = running.get(container);
        if (runningLayoutAnimtion != null)
        {
            runningLayoutAnimtion.cancel();
            running.remove(container);
        }

        // Execute the layout animation. When it is finished,
        // the callback will remove it from the map of
        // running layout animations
        final LayoutAnimation layoutAnimtion =
            new LayoutAnimation(container, newLayout);
        running.put(container, layoutAnimtion);
        layoutAnimtion.execute(durationS, new LayoutAnimationCallback()
        {
            @Override
            public void animationFinished()
            {
                running.remove(layoutAnimtion);
            }
        });
    }

    /**
     * Interface for classes that may be called when
     * a {@link LayoutAnimation} is finished.
     */
    private static interface LayoutAnimationCallback
    {
        /**
         * Will be called when the {@link LayoutAnimation} is finished
         */
        void animationFinished();
    }

    /**
     * A layout animation. This class performs the animation between
     * an initial state of a container, towards the state that is
     * defined by applying a new layout to the container.
     */
    private static class LayoutAnimation
    {
        /**
         * The container on which the animation is performed
         */
        private final Container container;

        /**
         * The new layout towards which the container is animated
         */
        private final LayoutManager newLayout;

        /**
         * The timer that performs the actual layout
         */
        private final Timer timer;

        /**
         * The delay for the timer
         */
        private final int delayMS = 20;

        /**
         * Creates a new LayoutAnimation for the given container,
         * which animates towards the given layout.
         *
         * @param container The container
         * @param newLayout The new layout
         */
        LayoutAnimation(Container container, LayoutManager newLayout)
        {
            this.container = container;
            this.newLayout = newLayout;
            this.timer = new Timer(delayMS, null);
        }

        /**
         * Execute the animation. This will store the current state of
         * the container, compute the target state based on the new
         * layout, and perform an animation towards the new state
         * that will take the specified duration (in seconds).
         * When the animation is finished, the given callback will
         * be notified.
         *
         * @param durationS The duration for the animation, in seconds
         * @param layoutAnimatorCallback The callback that will be
         * notified when the animation is finished.
         */
        void execute(final double durationS,
            final LayoutAnimationCallback layoutAnimatorCallback)
        {

            // Store all old bounds of the components of the container
            final Map<Component, Rectangle> oldBounds =
                getAllBounds(container.getComponents());

            // Apply the new layout, and store the new bounds
            // of all components
            container.setLayout(newLayout);
            newLayout.layoutContainer(container);
            final Map<Component, Rectangle> newBounds =
                getAllBounds(container.getComponents());

            // Restore the old bounds
            container.setLayout(null);
            setAllBounds(container.getComponents(), oldBounds);

            // Create the bounds that will be animated
            final Map<Component, Rectangle> currentBounds =
                getAllBounds(container.getComponents());

            // Set up the timer that will perform the animation
            timer.addActionListener(new ActionListener()
            {
                /**
                 * The current alpha value decribing the interpolation
                 * state, between 0 and 1
                 */
                double alpha = 0;

                /**
                 * The step size for the alpha.
                 */
                double alphaStep = 1.0 / (durationS * (1000.0 / delayMS));

                @Override
                public void actionPerformed(ActionEvent e)
                {
                    if (alpha == 1.0)
                    {
                        timer.stop();
                        container.setLayout(newLayout);
                        layoutAnimatorCallback.animationFinished();
                    }
                    alpha += alphaStep;
                    alpha = Math.min(1.0, alpha);

                    interpolate(oldBounds, newBounds, currentBounds, alpha);
                    setAllBounds(container.getComponents(), currentBounds);
                }
            });
            timer.setCoalesce(true);
            timer.start();
        }

        /**
         * Cancel this animation
         */
        void cancel()
        {
            timer.stop();
        }
    }


    /**
     * Create a map from the given components to their bounds.
     *
     * @param components The components
     * @return The resulting map
     */
    private static Map<Component, Rectangle> getAllBounds(
        Component components[])
    {
        Map<Component, Rectangle> currentBounds =
            new HashMap<Component, Rectangle>();
        for (Component component : components)
        {
            Rectangle bounds = component.getBounds();
            currentBounds.put(component, bounds);
        }
        return currentBounds;
    }

    /**
     * Set the bounds of the given components to the bounds that
     * are stored in the given map.
     *
     * @param components The components
     * @param newBounds The new bounds of the components
     */
    private static void setAllBounds(
        Component components[], Map<Component, Rectangle> newBounds)
    {
        for (Component component : components)
        {
            Rectangle bounds = newBounds.get(component);
            component.setBounds(bounds);
            component.validate();
        }
    }


    /**
     * Interpolate between all rectangles from the maps <code>b0</code>
     * and <code>b1</code> according to the given alpha value
     * (between 0 and 1), and store the interpolated rectangles
     * in <code>b</code>
     *
     * @param b0 The first input rectangles
     * @param b1 The second input rectangles
     * @param b The interpolated rectangles
     * @param alpha The alpha value, between 0 and 1
     */
    private static void interpolate(
        Map<Component, Rectangle> b0, Map<Component, Rectangle> b1,
        Map<Component, Rectangle> b, double alpha)
    {
        for (Component component : b0.keySet())
        {
            Rectangle r0 = b0.get(component);
            Rectangle r1 = b1.get(component);
            Rectangle r = b.get(component);
            interpolate(r0, r1, r, alpha);
        }
    }

    /**
     * Linearly interpolate between <code>r0</code> and <code>r1</code>
     * according to the given alpha value (between 0 and 1), and store
     * the result in <code>r</code>.
     *
     * @param r0 The first rectangle
     * @param r1 The second rectangle
     * @param r The interpolated rectangle
     * @param alpha
     */
    private static void interpolate(
        Rectangle r0, Rectangle r1, Rectangle r, double alpha)
    {
        r.x = (int)(r0.x + alpha * (r1.x - r0.x));
        r.y = (int)(r0.y + alpha * (r1.y - r0.y));
        r.width = (int)(r0.width + alpha * (r1.width - r0.width));
        r.height = (int)(r0.height + alpha * (r1.height - r0.height));
    }


    /**
     * Private constructor to prevent instantiation
     */
    private LayoutAnimator()
    {
    }

}

A small demo:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BoundedRangeModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;

// Demo for the LayoutAnimator
public class LayoutAnimatorDemo
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Create a component where optimized drawing
        // is disabled, to avoid flickering when
        // components overlap
        JComponent c = new JComponent()
        {
            private static final long serialVersionUID =
                -8793865141504880212L;
            @Override
            public boolean isOptimizedDrawingEnabled()
            {
                return false;
            }
        };
        f.setContentPane(c);

        Container container = f.getContentPane();
        container.setLayout(new FlowLayout());

        // Create buttons to switch between layouts
        JButton c0 = new JButton("FlowLayout");
        JButton c1 = new JButton("GridLayout");
        JButton c2 = new JButton("BorderLayout");
        JButton c3 = new JButton("GridBagLayout");

        // Create a slider for the animation duration
        JComponent c4 = new JPanel(new BorderLayout());
        c4.add(new JLabel("Duration (ms) :"), BorderLayout.WEST);
        JSlider slider = new JSlider(0, 2000);
        slider.setMinimumSize(new Dimension(100, 100));
        slider.setPaintTicks(true);
        slider.setMajorTickSpacing(500);
        slider.setPaintLabels(true);
        c4.add(slider, BorderLayout.CENTER);
        BoundedRangeModel b = slider.getModel();

        // Attach ActionListeners to the buttons that perform
        // animations to the different layouts
        connect(c0, container, new FlowLayout(), b);
        connect(c1, container, new GridLayout(2,3), b);
        connect(c2, container, createBorderLayout(c0, c1, c2, c3, c4), b);
        connect(c3, container, createGridBagLayout(c0, c1, c2, c3, c4), b);

        container.add(c0);
        container.add(c1);
        container.add(c2);
        container.add(c3);
        container.add(c4);

        f.setSize(800, 600);
        f.setVisible(true);
    }

    // Attach an ActionListener to the given button that will animate
    // the contents of the given container towards the given layout,
    // with a duration (in milliseconds) that is taken from the
    // given BoundedRangeModel
    private static void connect(
        JButton button, final Container container,
        final LayoutManager layoutManager,
        final BoundedRangeModel boundedRangeModel)
    {
        button.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                double durationS = boundedRangeModel.getValue() / 1000.0;
                LayoutAnimator.execute(container, layoutManager, durationS);
            }
        });
    }

    // Create a predefined BorderLayout
    private static LayoutManager createBorderLayout(
        Component c0, Component c1, Component c2, Component c3, Component c4)
    {
        BorderLayout borderLayout = new BorderLayout();
        borderLayout.addLayoutComponent(c0, BorderLayout.NORTH);
        borderLayout.addLayoutComponent(c1, BorderLayout.CENTER);
        borderLayout.addLayoutComponent(c2, BorderLayout.SOUTH);
        borderLayout.addLayoutComponent(c3, BorderLayout.WEST);
        borderLayout.addLayoutComponent(c4, BorderLayout.EAST);
        return borderLayout;
    }

    // Create a predefined GridBagLayout
    private static LayoutManager createGridBagLayout(
        Component c0, Component c1, Component c2, Component c3, Component c4)
    {
        GridBagLayout gridBagLayout = new GridBagLayout();
        GridBagConstraints c = null;

        c = new GridBagConstraints();
        c.gridx = 0;
        c.gridy = 0;
        c.weightx = 0.5;
        c.weighty = 0.5;
        c.gridwidth = 2;
        c.fill = GridBagConstraints.BOTH;
        gridBagLayout.addLayoutComponent(c0, c);

        c = new GridBagConstraints();
        c.gridx = 2;
        c.gridy = 0;
        c.weightx = 0.5;
        c.weighty = 0.5;
        c.fill = GridBagConstraints.BOTH;
        gridBagLayout.addLayoutComponent(c1, c);

        c = new GridBagConstraints();
        c.gridx = 0;
        c.gridy = 1;
        c.weightx = 0.25;
        c.weighty = 0.5;
        c.fill = GridBagConstraints.BOTH;
        gridBagLayout.addLayoutComponent(c2, c);

        c = new GridBagConstraints();
        c.gridx = 1;
        c.gridy = 1;
        c.weightx = 0.75;
        c.weighty = 0.5;
        c.fill = GridBagConstraints.BOTH;
        gridBagLayout.addLayoutComponent(c3, c);

        c = new GridBagConstraints();
        c.gridx = 2;
        c.gridy = 1;
        c.weightx = 0.5;
        c.weighty = 0.5;
        c.fill = GridBagConstraints.BOTH;
        gridBagLayout.addLayoutComponent(c4, c);

        return gridBagLayout;
    }

}
like image 68
Marco13 Avatar answered Sep 20 '22 14:09

Marco13


Basically, the "animation" is happening but the screen is refreshed only in the end.

Don't use a sleep(..) method when code executes on the Event Dispatch Thread and all code from Swing listeners do execute on the EDT.

I tried to use a swing Timer

Yes, that is the proper approach, but you are using the Timer incorrectly. You should NOT have a loop in the Timer code. That is the point of using a Timer. You schedule the Timer to fire whenever you want the animation. In your case is looks like you want the animation every 10ms (which may be too fast), so you should schedule the Timer to fire every 10ms.

Since you are using a null layout there is no need for the revalidate() (which is used to invoke the layout manager) or the repaint, because when you change the size/location of a component a repaint() is automatically invoked.

like image 23
camickr Avatar answered Sep 21 '22 14:09

camickr