swing page transition (fade in / out)

activity
initial post: 01 dec 2008
last update: 02 dec 2008

Few months ago I wanted some kind of page transition effect for a wizard I made in swing (an applet).
I found a nice effect in examples provided with an older release of JXLayer (from dev.java.net). The demo is still available at http://swinghelper.dev.java.net (the TabbedPaneAnimationDemo.jnlp link).
Now the project is stand alone (http://jxlayer.dev.java.net) and in version 3 was a major api change but the demo was not ported to the new JXLayer api.

I make some tries to port the TabbedPaneAnimationDemo to latest JXLayer, I failed, and I decide to implement the effect myself.

Here is my approach:

I have:

1. a container(a JXPanel that I named FadeEffectTransitionContainer) on with I set as layout a CardLayout. - This is the container that holds all wizard pages and the pages are switched with CardLayout.show().
2. and wizard pages that are also JXPanels.

The effect I wanted to be the same as that provided by TabbedPaneAnimationDemo: old wizard page disappear with diminishing alpha while the new page appear with increasing alpha.

For a while I was trying to override paint (or paintComponents) in main panel and from this method to call paint on child panels. I was not able to make it work in that way. So I will tell you how I finally make this effect.

When the page in wizard is about to change, I make 2 screenshots of the 2 panels involved in page transition:

private BufferedImage takePicture(JXPanel aPanel){
    BufferedImage image = createBuffer(aPanel.getWidth(), aPanel.getHeight());
    Graphics g = image.getGraphics();
    aPanel.paint(g);
    g.dispose();
    return image;
}

private BufferedImage createBuffer(int width, int height) {
    return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}


And this 2 images are painted on the FadeEffectTransitionContainer in paint(Graphics g). First the old panel(the picture of it) then the new panel with increasing alpha.

Here is a screen-shot:



I see that with some containers (JPanel, JXRadioGroup etc.) added in my pages, if I make them transparent (setOpaque(false)), I have some garbage on them when I make the images. So I make them opaque. And after that I realize that I don't need to make a diminishing alpha on the old page that disappear because the page that appear been opaque I get the same effect.


Here is the main container of the wizard:

FadeEffectTransitionContainer.java
package ro.arhinet.scan.ui.effects.transition;

import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.math.BigDecimal;
import org.jdesktop.swingx.JXPanel;

/**
 *
 * @author Mihai Vasilache
 */
public class FadeEffectTransitionContainer extends JXPanel{
    private boolean isPaintingEffects;

    public BufferedImage oldPanelPicture;
    public BufferedImage newPanelPicture;

    private BigDecimal alpha = new BigDecimal("1");;
    
    @Override
    public void paint(Graphics g){
        if( ! isPaintingEffects){
            super.paint(g);
            return;
        }

        eraseBackground(g);
        doCustomPaint(g);
    }

    private void eraseBackground(Graphics g){
        g.setColor(getBackground());
        g.fillRect(0, 0, getWidth(), getHeight());
    }

    private void doCustomPaint(Graphics g_param){
        //paint old panel with no alpha
        Graphics2D g2 = (Graphics2D) g_param.create();
        g2.drawImage(oldPanelPicture, 0, 0, null);
        g2.dispose();

        //paint the new panel on top of old panel
        //because of panels been opaque the old panel apear with dimishing alpha
        Graphics2D g3 = (Graphics2D) g_param.create();
        setAlphaOnGraphics((Graphics2D)g3);
        g3.drawImage(newPanelPicture, 0, 0, null);
        g3.dispose();
    }

    protected void setAlphaOnGraphics(Graphics2D g2) {
        BigDecimal negativeAlpha = new BigDecimal("1").subtract(alpha);
        Composite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, negativeAlpha.floatValue());
        g2.setComposite(composite);
    }

    public void startPaintingEffects(){
        isPaintingEffects = true;
    }

    public void stopPaintingEffects(){
        isPaintingEffects = false;
    }

    public void setOldPanelPicture(BufferedImage thePicture){
        oldPanelPicture = thePicture;
    }

    public void setNewPanelPicture(BufferedImage thePicture){
        newPanelPicture = thePicture;
    }

    public void setAlphaTransition(BigDecimal alpha) {
        this.alpha = alpha;
    }

    public BigDecimal getAlphaTransition() {
        return alpha;
    }
}



And this is the class that controls the alpha and has a timer to apply the effect:

PanelChangeAnimator.java
package ro.arhinet.scan.ui.effects.transition;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.math.BigDecimal;
import javax.swing.Timer;
import org.jdesktop.swingx.JXPanel;

public class PanelChangeAnimator {

    private Timer timer;
    private float delta;
    private BigDecimal alpha = new BigDecimal("1");

    JXPanel oldPanel;
    JXPanel newPanel;
    FadeEffectTransitionContainer effectsContainer;
    private static PanelChangeAnimator instanceRunning;

    public PanelChangeAnimator() {
        this(50, .05f);
    }

    private PanelChangeAnimator(int delay, final float delta) {
        setDelta(delta);
        timer = new Timer(delay, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (effectsContainer.getAlphaTransition().floatValue() == 0.0f) {
                    timer.stop();
                    effectsContainer.stopPaintingEffects();
                    synchronized(PanelChangeAnimator.class){
                        instanceRunning = null;
                    }
                    return;
                }
                alpha = alpha.subtract(new BigDecimal(String.valueOf(delta)));
                BigDecimal negativeAlpha = new BigDecimal("1").subtract(alpha);
                effectsContainer.setAlphaTransition(alpha);
                //otherwise the child components of the new Panel (visible one
                //in CardLayout) paints themselfs with alpha=1 (on mouse over)
                newPanel.setAlpha(negativeAlpha.floatValue());
                effectsContainer.repaint();
            }
        });
    }

    public void setDelta(float delta) {
        if (delta <= 0 || delta > 1) {
            throw new IllegalArgumentException();
        }
        this.delta = delta;
    }

    private boolean stopPrevious(){
        synchronized(PanelChangeAnimator.class){
            if (instanceRunning == null){
                instanceRunning = this;
                return false;
            }

            instanceRunning.timer.stop();
            instanceRunning.effectsContainer.stopPaintingEffects();
            instanceRunning.newPanel.setAlpha(1f);
            instanceRunning = this;
            return true;
        }
    }

    public void changePanels(JXPanel newPanel, JXPanel oldPanel, FadeEffectTransitionContainer container) {
        stopPrevious();
        this.effectsContainer = container;
        this.oldPanel = oldPanel;
        this.newPanel = newPanel;
        this.alpha = new BigDecimal("1");

        takePicturesOfPanels();
        
        effectsContainer.setAlphaTransition(alpha);
        effectsContainer.startPaintingEffects();
        effectsContainer.repaint();
        timer.start();
    }

    private void takePicturesOfPanels(){
        boolean oldPanelOldVisible = oldPanel.isVisible();
        float oldPanelOldAlpha = oldPanel.getAlpha();
        oldPanel.setVisible(true);
        oldPanel.setBackground(Color.WHITE);
        oldPanel.setOpaque(true);
        oldPanel.setAlpha(1f);
        effectsContainer.setOldPanelPicture(takePicture(oldPanel));
        oldPanel.setVisible(oldPanelOldVisible);
        oldPanel.setAlpha(oldPanelOldAlpha);

        boolean newPanelOldVisible = newPanel.isVisible();
        float newPanelOldAlpha = newPanel.getAlpha();
        newPanel.setVisible(true);
        newPanel.setBackground(Color.WHITE);
        newPanel.setOpaque(true);
        newPanel.setAlpha(1f);
        effectsContainer.setNewPanelPicture(takePicture(newPanel));
        newPanel.setVisible(newPanelOldVisible);
        newPanel.setAlpha(newPanelOldAlpha);

//      try{
//          String fname = System.currentTimeMillis() + "";
//          ImageIO.write(effectsContainer.b1, "gif", new File("c:\\TEMP\\io\\" + fname + ".gif"));
//          ImageIO.write(effectsContainer.b2, "gif", new File("c:\\TEMP\\io\\" + fname + "_2.gif"));
//      }catch(Exception e){
//          e.printStackTrace();
//          throw new RuntimeException(e);
//      }
    }

    private BufferedImage takePicture(JXPanel aPanel){
        BufferedImage image = createBuffer(aPanel.getWidth(), aPanel.getHeight());
        Graphics g = image.getGraphics();
        aPanel.paint(g);
        g.dispose();
        return image;
    }

    protected BufferedImage createBuffer(int width, int height) {
        return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    }


}


And pages are changed with these lines of code:

    ((CardLayout) container.getLayout()).show(container,  newPanelName);
    new PanelChangeAnimator().changePanels(newPanel, currentPanel, container);


And of course, the container is built in that way:

    CardLayout cardLayout = new CardLayout();
    container = new FadeEffectTransitionContainer();
    container.setOpaque(true);
    container.setBackground(Color.WHITE);
    container.setLayout(cardLayout);
    ....
    container.add(aStepPanel, aStepName);
    ....

No comments: