View RSS Feed

ozzyman

Simple Frame Transitions without Complex Code

Rating: 1 votes, 5.00 average.
by , 03-22-2012 at 08:18 PM (7175 Views)
I've spent the past few days looking up Java window transitions that work with Swing components & containers (JFrame, JPanel, etc.).

Yes, there are packages such as JavaFX which includes a Timeline & Transition framework. I also came across these:

Trident:
Project Kenai — We're More Than Just a Forge

FRC:
Filthy Rich Clients (find the Animation jar (org.jdesktop.animation))

However, I've been impatient lately and didn't feel like cluttering my code with framework-code I barely understood. I wanted something fast and I didn't care if it was dirty, so long as I didn't have to set up and learn another package.

Besides, those packages were written to include many other features which I didn't want to know about, and I like doing things my way sometimes.

In the end, I arrived with this solution:

1. Create a CardLayout with the many JPanels I wanted to switch between.
2. Get ready to show the next Card
2.i) capture the graphics of the JPanel i'm about to hide
2.ii) capture the graphics of the JPanel i'm about to show
2.iii) merge the two images into one
2.iv) add the image to a new intermediary Card using JScrollPane/something scrollable
2.v) temporarily add the intermediary Card to the CardLayout
2.v.a) show it
2.v.b) scroll it to the end
2.v.c) remove it
2.vi) finally, show the next Card

Here's how I did it:

I started off with the ImageHelper class to perform some functions:

Java Code:
/**
 *
 * @author Ozzy
 */
public class ImageHelper {

    // ImageHelper to help me capture images of Swing Components and
    // merge them into a single image.

    // Capture a Swing Component and return as a BufferedImage
    public static BufferedImage captureComponent(Component component) {
        BufferedImage image = new BufferedImage(component.getWidth(), component.getHeight(),
                BufferedImage.TYPE_INT_RGB);
        component.paint(image.getGraphics());
        return image;
    }
    
    // Some constant to define how I'd like to merge my images
    public static final int SIDE_BY_SIDE = 0;

    // Helper method to combine two images, in the format I specify
    public static BufferedImage combineImages(BufferedImage img1, BufferedImage img2, int renderHint) {
        switch (renderHint) {
            default:
            case SIDE_BY_SIDE:
                // Create a new image that is the width of img1+img2
                // Take the height of the taller image
                // Paint the two images side-by-side
                BufferedImage combined = new BufferedImage(img1.getWidth()+img2.getWidth(),
                        Math.max(img1.getHeight(), img2.getHeight()), BufferedImage.TYPE_INT_RGB);
                Graphics g = combined.getGraphics();
                g.drawImage(img1, 0, 0, null);
                g.drawImage(img2, img1.getWidth(), 0, null);
                return combined;
        }
    }

}


Now I wanted some way to Scroll my Image left-to-right or right-to-left depending on my input parameter. It would be a custom TimerTask that would sit in my helper class to do the animation. I called it the Framinator class (Frame+Animator).

Java Code:
/**
 *
 * @author Ozzy
 */
public class Framinator {
    // The rest will be revealed below
    ...

    // Some constants which define the transition direction
    public static final int TRANSITION_LEFT = 0;
    public static final int TRANSITION_RIGHT = 1;
    
    private static class ScrollTransition extends TimerTask {
        
        // TimerTask to scroll a JScrollPane in a given direction
        // and then call a callback TimerTask.

        // Usage:
        // JScrollPane scrollPane = ...
        // ...
        // Timer t = new Timer();
        // TimerTask callback = new TimerTask() {
        //    public void run() {
        //        ...
        //    }
        // }
        // t.scheduleAtFixedRate(t, callback, scrollPane, 20, TRANSITION_LEFT)

        private Timer timer;
        private TimerTask callback;
        private JScrollPane scroller;
        private int step;
        private int direction;
        private int $offset = 0;
        private int $max = 0;
        private boolean $runOnce = true;

        ScrollTransition(Timer timer, TimerTask callback, JScrollPane scroller, int step, int direction) {
            this.timer = timer;
            this.callback = callback;
            this.scroller = scroller;
            this.step = step;
            this.direction = direction;
            $max = scroller.getHorizontalScrollBar().getMaximum();
        }

        @Override
        public void run() {
            switch(direction) {
                default:
                case TRANSITION_LEFT:
                    // Going Right-to-Left
                    // Start at the end, scroll backwards
                    if ($runOnce) {
                        scroller.getHorizontalScrollBar().setValue($max);
                        $offset = $max;
                        $runOnce = false;
                    }

                    // If there is space for a step, scroll left one step
                    // Else scroll right to the left
                    if ($offset > (0+step)) {
                        $offset -= step;
                        scroller.getHorizontalScrollBar().setValue($offset);
                    } else {
                        scroller.getHorizontalScrollBar().setValue(0);
                        // Cancel the current ScrollTransition
                        cancel();
                        timer.schedule(callback, 0);
                    }
                    break;
                    
                case TRANSITION_RIGHT:
                    // Going Left-to-Right
                    // If there is space for a step, scroll right one step
                    // Else scroll right to the end
                    if ($offset < ($max-step)) {
                        $offset += step;
                        scroller.getHorizontalScrollBar().setValue($offset);
                    } else {
                        scroller.getHorizontalScrollBar().setValue($max);
                        // Cancel the current ScrollTransition
                        cancel();
                        timer.schedule(callback, 0);
                    }
                    break;
            }
        }

    }
}


Finally I just needed a helper method in my Framinator class to actually perform the Tasks.
Here's how it went:

Java Code:
/**
 *
 * @author Ozzy
 */
class Framinator {

    public static void transitionComponents(final Container parent, Component from, final Component to, int direction) {
        // First check we have a CardLayout, if not: return
        final CardLayout cl;
        if (parent.getLayout() instanceof CardLayout) {
            cl = (CardLayout) parent.getLayout();
        } else return;

        // Create the combined image depending on the direction
        BufferedImage combined;
        switch(direction) {
            default:
            case TRANSITION_LEFT:
                combined = ImageHelper.combineImages(
                ImageHelper.captureComponent(to),
                ImageHelper.captureComponent(from),
                ImageHelper.SIDE_BY_SIDE);
                break;
            case TRANSITION_RIGHT:
                combined = ImageHelper.combineImages(
                ImageHelper.captureComponent(from),
                ImageHelper.captureComponent(to),
                ImageHelper.SIDE_BY_SIDE);
                break;
        }

        // Create the intermediary transition panel
        JPanel transition = new JPanel(new GridLayout());
        transition.add(new JLabel(new ImageIcon(combined)));
        final JScrollPane scroller = new JScrollPane(transition);
            // Hide the scroll bars
            scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
            scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        // Add the scroller to the CardLayout and show it
        parent.add(scroller, "transitionCard");
        cl.show(parent, "transitionCard");

        // Prepare the Timer and the callback task
        Timer scrollTimer = new Timer();
        TimerTask callback = new TimerTask() {
            @Override
            public void run() {
                // Once the ScrollTransition has finished:
                // Remove the intermediary transition Card and show the next Card
                parent.remove(scroller);
                parent.add(to, "newCard");
                cl.show(parent, "newCard");
            }
        };

        // Start the ScrollTransition
        ScrollTransition scrollTask = new ScrollTransition(scrollTimer, callback, scroller, 20, direction);
        scrollTimer.scheduleAtFixedRate(scrollTask, 0, 30);
    }

    // ScrollTransition nested class as above
    ...
}

Now everything is ready, we can use this class like this:

Java Code:
public class TransitionPanel extends JPanel {
    TransitionPanel() {
        super(new CardLayout());
    }
    public void transitionLeft(Component from, Component to) {
        Framinator.transitionComponents(this, from, to, Framinator.TRANSITION_LEFT);
    }
    public void transitionRight(Component from, Component to) {
        Framinator.transitionComponents(this, from, to, Framinator.TRANSITION_RIGHT);
    }
}

And that's it. Of course you could choose to be more imaginative with the image effect/scrolling.

I'm using a Gesture listener so that I can transition the JPanels depending on a user swipe Gesture (for touch screen PCs).

Please leave your comments, complaints and suggestions.

Submit "Simple Frame Transitions without Complex Code" to Facebook Submit "Simple Frame Transitions without Complex Code" to Digg Submit "Simple Frame Transitions without Complex Code" to del.icio.us Submit "Simple Frame Transitions without Complex Code" to StumbleUpon Submit "Simple Frame Transitions without Complex Code" to Google

Comments

  1. noobplus's Avatar
    • |
    • permalink
    Hi,
    I've made a JPanel with a paintComponent() inside.
    I'v attached to a scrollpane, and the scrollpane is in a JFrame()
    I got the scrollbars,
    but when i scroll the bars, I'm getting new pixels drawn in new area, kind of overlapping, (the scrollpane before scrolling shows me the correct pixels)
    the pixels are drawn from "drawLine()" actually.
    should we bring super.cardLayout() here?
    Or is ti a runtime error?

    regards
    dhilip