Creating Fluid Experiences: Transition and Parallel Animation in Java

Animation brings applications to life, providing visual feedback, guiding user attention, and creating polished user experiences. In Java, animations can range from simple property transitions to complex parallel sequences. This article explores the fundamental concepts and practical implementation of transitions and parallel animations across Java's UI toolkits.


Part 1: Animation in Swing with Java2D

While Swing doesn't have built-in animation support like JavaFX, we can create smooth animations using timers and property interpolation.

Basic Animation Concepts

Key Animation Principles:

  • Interpolation: Calculating intermediate values between start and end states
  • Frame Rate: Controlling update frequency (typically 30-60 FPS)
  • Easing: Non-linear animation for more natural movement

Simple Transition with Swing Timer

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class BasicSwingAnimation extends JPanel {
private int circleX = 50;
private int circleY = 100;
private int targetX = 300;
private final int circleSize = 40;
private Timer timer;
private float progress = 0.0f;
private final int animationDuration = 1000; // milliseconds
private long startTime;
public BasicSwingAnimation() {
setPreferredSize(new Dimension(400, 200));
// Create animation timer (60 FPS)
timer = new Timer(16, new ActionListener() { // ~60 FPS
@Override
public void actionPerformed(ActionEvent e) {
updateAnimation();
repaint();
}
});
// Start button
JButton startButton = new JButton("Start Animation");
startButton.addActionListener(e -> startAnimation());
setLayout(new BorderLayout());
add(this, BorderLayout.CENTER);
add(startButton, BorderLayout.SOUTH);
}
private void startAnimation() {
progress = 0.0f;
startTime = System.currentTimeMillis();
circleX = 50; // Reset position
timer.start();
}
private void updateAnimation() {
long currentTime = System.currentTimeMillis();
long elapsed = currentTime - startTime;
progress = Math.min(1.0f, (float) elapsed / animationDuration);
// Linear interpolation
circleX = (int) (50 + (targetX - 50) * progress);
// Add easing (cubic ease-out)
float easedProgress = 1 - (1 - progress) * (1 - progress) * (1 - progress);
circleY = (int) (100 + 50 * Math.sin(easedProgress * Math.PI));
if (progress >= 1.0f) {
timer.stop();
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// Enable anti-aliasing for smoother graphics
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
RenderingHints.VALUE_ANTIALIAS_ON);
// Draw background
g2d.setColor(Color.LIGHT_GRAY);
g2d.fillRect(0, 0, getWidth(), getHeight());
// Draw animated circle
g2d.setColor(Color.BLUE);
g2d.fillOval(circleX, circleY, circleSize, circleSize);
// Draw progress text
g2d.setColor(Color.BLACK);
g2d.drawString(String.format("Progress: %.1f%%", progress * 100), 10, 20);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Swing Animation Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new BasicSwingAnimation());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}

Advanced Transition with Multiple Properties

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
public class MultiPropertyAnimation extends JPanel {
private float progress = 0.0f;
private Timer timer;
private long startTime;
private final int duration = 2000;
// Animated properties
private Color color = Color.RED;
private float scale = 1.0f;
private float rotation = 0.0f;
private float opacity = 1.0f;
public MultiPropertyAnimation() {
setPreferredSize(new Dimension(500, 300));
timer = new Timer(16, e -> {
updateAnimation();
repaint();
});
JButton startBtn = new JButton("Start Complex Animation");
startBtn.addActionListener(e -> startAnimation());
setLayout(new BorderLayout());
add(this, BorderLayout.CENTER);
add(startBtn, BorderLayout.SOUTH);
}
private void startAnimation() {
progress = 0.0f;
startTime = System.currentTimeMillis();
timer.start();
}
private void updateAnimation() {
long currentTime = System.currentTimeMillis();
long elapsed = currentTime - startTime;
progress = Math.min(1.0f, (float) elapsed / duration);
// Apply different easing to different properties
float easeOut = 1 - (1 - progress) * (1 - progress);
float easeIn = progress * progress;
float elastic = (float) (1 + Math.sin(progress * Math.PI * 5) * 
(1 - progress) * 0.3);
// Animate multiple properties
scale = 0.5f + easeOut * 1.5f; // Scale from 0.5x to 2x
rotation = easeIn * 360; // Rotate 360 degrees
opacity = 0.3f + easeOut * 0.7f; // Fade in
// Color transition from red to blue to green
if (progress < 0.5f) {
float colorProgress = progress * 2;
color = interpolateColor(Color.RED, Color.BLUE, colorProgress);
} else {
float colorProgress = (progress - 0.5f) * 2;
color = interpolateColor(Color.BLUE, Color.GREEN, colorProgress);
}
if (progress >= 1.0f) {
timer.stop();
}
}
private Color interpolateColor(Color start, Color end, float progress) {
int r = (int) (start.getRed() + (end.getRed() - start.getRed()) * progress);
int g = (int) (start.getGreen() + (end.getGreen() - start.getGreen()) * progress);
int b = (int) (start.getBlue() + (end.getBlue() - start.getBlue()) * progress);
return new Color(r, g, b);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
RenderingHints.VALUE_ANTIALIAS_ON);
// Draw background
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, getWidth(), getHeight());
// Set composite for opacity
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
// Save original transform
AffineTransform originalTransform = g2d.getTransform();
// Apply transformations at center
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
g2d.translate(centerX, centerY);
g2d.scale(scale, scale);
g2d.rotate(Math.toRadians(rotation));
// Draw shape
Shape shape = new Ellipse2D.Float(-50, -50, 100, 100);
g2d.setColor(color);
g2d.fill(shape);
// Restore transform
g2d.setTransform(originalTransform);
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
// Draw info
g2d.setColor(Color.BLACK);
g2d.drawString(String.format("Scale: %.2f, Rotation: %.0f°", scale, rotation), 10, 20);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Multi-Property Animation");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MultiPropertyAnimation());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}

Part 2: Parallel Animations in JavaFX

JavaFX provides a comprehensive animation framework that makes parallel and sequential animations much easier to implement.

Basic JavaFX Transition

import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class JavaFXBasicTransition extends Application {
@Override
public void start(Stage primaryStage) {
Pane root = new Pane();
root.setPrefSize(400, 200);
Circle circle = new Circle(50, 100, 20, Color.BLUE);
root.getChildren().add(circle);
// Create basic translation transition
TranslateTransition transition = new TranslateTransition(Duration.seconds(2), circle);
transition.setFromX(0);
transition.setToX(300);
transition.setCycleCount(1);
transition.setAutoReverse(false);
// Add easing (bounce effect at the end)
transition.setInterpolator(Interpolator.EASE_BOTH);
Scene scene = new Scene(root);
primaryStage.setTitle("JavaFX Basic Transition");
primaryStage.setScene(scene);
primaryStage.show();
// Start animation after short delay
PauseTransition delay = new PauseTransition(Duration.seconds(1));
delay.setOnFinished(e -> transition.play());
delay.play();
}
}

Parallel Animations in JavaFX

import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class JavaFXParallelAnimation extends Application {
@Override
public void start(Stage primaryStage) {
Pane root = new Pane();
root.setPrefSize(600, 400);
Rectangle rect = new Rectangle(50, 50, 60, 40);
rect.setFill(Color.CRIMSON);
root.getChildren().add(rect);
// Create multiple parallel transitions
TranslateTransition moveTransition = new TranslateTransition(Duration.seconds(3), rect);
moveTransition.setToX(400);
moveTransition.setToY(200);
RotateTransition rotateTransition = new RotateTransition(Duration.seconds(3), rect);
rotateTransition.setByAngle(720); // Two full rotations
ScaleTransition scaleTransition = new ScaleTransition(Duration.seconds(3), rect);
scaleTransition.setToX(2.0);
scaleTransition.setToY(2.0);
FillTransition colorTransition = new FillTransition(Duration.seconds(3), rect);
colorTransition.setFromValue(Color.CRIMSON);
colorTransition.setToValue(Color.DARKBLUE);
// Create parallel animation group
ParallelTransition parallelTransition = new ParallelTransition();
parallelTransition.getChildren().addAll(
moveTransition, rotateTransition, scaleTransition, colorTransition
);
parallelTransition.setCycleCount(2);
parallelTransition.setAutoReverse(true);
Scene scene = new Scene(root);
primaryStage.setTitle("JavaFX Parallel Animation");
primaryStage.setScene(scene);
primaryStage.show();
parallelTransition.play();
}
}

Complex Sequential and Parallel Animation

import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ComplexAnimationSequence extends Application {
@Override
public void start(Stage primaryStage) {
Pane root = new Pane();
root.setPrefSize(600, 400);
Circle circle1 = new Circle(50, 100, 25, Color.RED);
Circle circle2 = new Circle(50, 200, 25, Color.BLUE);
Circle circle3 = new Circle(50, 300, 25, Color.GREEN);
root.getChildren().addAll(circle1, circle2, circle3);
// Animation for circle 1: Move right while fading
TranslateTransition move1 = new TranslateTransition(Duration.seconds(2), circle1);
move1.setToX(400);
FadeTransition fade1 = new FadeTransition(Duration.seconds(2), circle1);
fade1.setToValue(0.3);
ParallelTransition anim1 = new ParallelTransition(move1, fade1);
// Animation for circle 2: Move with bounce and color change
TranslateTransition move2 = new TranslateTransition(Duration.seconds(1.5), circle2);
move2.setToX(400);
move2.setInterpolator(Interpolator.EASE_OUT);
FillTransition color2 = new FillTransition(Duration.seconds(1.5), circle2);
color2.setToValue(Color.PURPLE);
ParallelTransition anim2 = new ParallelTransition(move2, color2);
// Animation for circle 3: Complex path with rotation
PathTransition path3 = new PathTransition();
path3.setDuration(Duration.seconds(2));
path3.setNode(circle3);
path3.setPath(new javafx.scene.shape.Line(50, 300, 400, 100));
RotateTransition rotate3 = new RotateTransition(Duration.seconds(2), circle3);
rotate3.setByAngle(360);
ParallelTransition anim3 = new ParallelTransition(path3, rotate3);
// Create sequential timeline with delays
SequentialTransition sequence = new SequentialTransition(
anim1,
new PauseTransition(Duration.seconds(0.5)),
anim2,
new PauseTransition(Duration.seconds(0.5)),
anim3
);
sequence.setCycleCount(Animation.INDEFINITE);
sequence.setAutoReverse(true);
Scene scene = new Scene(root);
primaryStage.setTitle("Complex Animation Sequence");
primaryStage.setScene(scene);
primaryStage.show();
sequence.play();
}
}

Custom Timeline Animation in JavaFX

For maximum control, use the Timeline class:

import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CustomTimelineAnimation extends Application {
@Override
public void start(Stage primaryStage) {
Pane root = new Pane();
root.setPrefSize(400, 400);
Circle circle = new Circle(200, 200, 50, Color.ORANGE);
root.getChildren().add(circle);
// Create custom timeline with keyframes
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(circle.centerXProperty(), 100),
new KeyValue(circle.centerYProperty(), 100),
new KeyValue(circle.fillProperty(), Color.ORANGE)
),
new KeyFrame(Duration.seconds(1),
new KeyValue(circle.centerXProperty(), 300),
new KeyValue(circle.centerYProperty(), 100),
new KeyValue(circle.fillProperty(), Color.RED)
),
new KeyFrame(Duration.seconds(2),
new KeyValue(circle.centerXProperty(), 300),
new KeyValue(circle.centerYProperty(), 300),
new KeyValue(circle.fillProperty(), Color.PURPLE)
),
new KeyFrame(Duration.seconds(3),
new KeyValue(circle.centerXProperty(), 100),
new KeyValue(circle.centerYProperty(), 300),
new KeyValue(circle.fillProperty(), Color.BLUE)
),
new KeyFrame(Duration.seconds(4),
new KeyValue(circle.centerXProperty(), 100),
new KeyValue(circle.centerYProperty(), 100),
new KeyValue(circle.fillProperty(), Color.ORANGE)
)
);
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(false);
Scene scene = new Scene(root);
primaryStage.setTitle("Custom Timeline Animation");
primaryStage.setScene(scene);
primaryStage.show();
timeline.play();
}
}

Animation Best Practices

Performance Optimization

  1. Swing:
  • Use repaint() only on visible regions when possible
  • Consider double buffering for complex animations
  • Use VolatileImage for frequently changing graphics
  1. JavaFX:
  • Leverage hardware acceleration
  • Use Cache and CacheHint for static parts of animated nodes
  • Avoid frequent layout passes during animations

Smooth Animation Tips

// Frame rate management in Swing
public class SmoothAnimationTimer {
private static final int TARGET_FPS = 60;
private static final long OPTIMAL_TIME = 1000000000 / TARGET_FPS;
public void startAnimation() {
new Thread(() -> {
long lastLoopTime = System.nanoTime();
while (true) {
long now = System.nanoTime();
long updateLength = now - lastLoopTime;
lastLoopTime = now;
double delta = updateLength / ((double)OPTIMAL_TIME);
// Update game state
updateAnimation(delta);
// Repaint on EDT
SwingUtilities.invokeLater(() -> repaint());
try {
long sleepTime = (lastLoopTime - System.nanoTime() + OPTIMAL_TIME) / 1000000;
if (sleepTime > 0) {
Thread.sleep(sleepTime);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
}
}

Easing Functions

public class EasingFunctions {
// Linear interpolation
public static float linear(float t) {
return t;
}
// Quadratic ease-in
public static float easeInQuad(float t) {
return t * t;
}
// Quadratic ease-out
public static float easeOutQuad(float t) {
return 1 - (1 - t) * (1 - t);
}
// Cubic ease-in-out
public static float easeInOutCubic(float t) {
return t < 0.5 ? 4 * t * t * t : 1 - (float)Math.pow(-2 * t + 2, 3) / 2;
}
// Elastic ease-out
public static float easeOutElastic(float t) {
float c4 = (float)(2 * Math.PI) / 3;
return t == 0 ? 0 : 
t == 1 ? 1 : 
(float)Math.pow(2, -10 * t) * (float)Math.sin((t * 10 - 0.75) * c4) + 1;
}
// Bounce ease-out
public static float easeOutBounce(float t) {
float n1 = 7.5625f;
float d1 = 2.75f;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5f / d1) * t + 0.75f;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25f / d1) * t + 0.9375f;
} else {
return n1 * (t -= 2.625f / d1) * t + 0.984375f;
}
}
}

Conclusion

Transition and parallel animations in Java can be implemented through different approaches:

  1. Swing/Java2D: Provides full control through manual animation loops with Timer and property interpolation
  2. JavaFX: Offers built-in transition classes and timeline-based animation with better performance

Key takeaways:

  • Use easing functions for natural-looking animations
  • Parallel animations create complex effects by running multiple transitions simultaneously
  • JavaFX is generally preferred for animation-heavy applications due to its hardware acceleration
  • Performance optimization is crucial for smooth animations, especially in Swing
  • Consider user experience - animations should enhance, not hinder, usability

Whether building traditional desktop applications with Swing or modern UIs with JavaFX, mastering animation techniques enables creating engaging, professional-quality Java applications that provide excellent user experiences.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper